* [PATCH v5 0/3] iommu/iova: convert from rbtree to maple tree
@ 2026-08-18 15:25 Rik van Riel
2026-08-18 15:25 ` [RFC PATCH 1/3] " Rik van Riel
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Rik van Riel @ 2026-08-18 15:25 UTC (permalink / raw)
To: linux-kernel
Cc: kernel-team, joro, will, robin.murphy, iommu, liam, maple-tree,
linux-mm, ashok.raj, jgg, kyle
Occasionally production workloads at Meta run into the linear search in
alloc_iova() in ways that cause real issues. For example, when enough
CPUs at a time fall into the linear search trap, systems have been known
to get stuck for so long that it causes soft lockups.
This series indexes the iova ranges in a maple tree instead. Its gap
search makes alloc_iova() O(log n).
struct iova loses its rb_node and shrinks from 40 to 16 bytes.
The maple tree keeps its nodes outside the entries, so total memory
use ends up about the same as before.
Patch 2 handles the one thing the maple tree does that an rbtree does
not: erasing an entry can result in the need to rebalance a tree, and
allocation of maple tree nodes.
iovas are freed from atomic context, and GFP_ATOMIC allocations mean
the erase can fail. When it does, the entry is marked IOVA_DEFERRED
in place and the struct iova is freed. The marker keeps the range
reserved until iova_drain_deferred() retries the erase.
Ashok Raj asked on v4 whether the marker store can fail in turn, since
the WARN_ON_ONCE there reads like error handling for a case the comment
claims cannot happen.
Code examination shows that, with the current maple tree code, the
IOVA_DEFERRED maple tree store will never result in an allocation,
and cannot fail. This series adds a test case which allows us to verify
that maple tree property continues to be true.
Only a corrupted tree, one no longer holding the iova at its own range,
can reach a store type that allocates. The WARN_ON_ONCE is more of an
assertion than a recovery path.
Liam Howlett's "maple_tree: lock checking and clean ups" series adds a
WARN_ON_ONCE to mas_nomem() for a GFP_ATOMIC store under an external
lock. This series is external-lock and GFP_ATOMIC by construction, so
both stores would splat if that lands as posted.
Liam, is the intent to disallow that combination, or to flag callers that
cannot tolerate a failed store? The iova code handles failure on both
paths.
The code was written with Claude, and nitpicked by myself. Don't be shy
if there are more nitpicks remaining.
Tested with the KUnit suite in a VM, including with PROVE_LOCKING,
DEBUG_MAPLE_TREE and KASAN enabled, and on an AMD Bergamo system with the
IOMMU enabled. I know of no way to reproduce the linear search soft
lockups at will, so that scenario stays unverified in practice.
drivers/iommu/.kunitconfig | 6 +
drivers/iommu/Kconfig | 16 +
drivers/iommu/Makefile | 1 +
drivers/iommu/iova-kunit.c | 544 +++++++++++++++++++++++++++++++++
drivers/iommu/iova.c | 561 ++++++++++++++++++++---------------
include/linux/iova.h | 21 +-
6 files changed, 901 insertions(+), 248 deletions(-)
---
v4: https://lore.kernel.org/r/20260624030853.2340880-1-riel@surriel.com
v5:
- subject prefix iommu/iova:, matching the file's history
- put_iova_domain() takes iova_lock across the tree walk and
__mt_destroy(); without it lockdep reports suspicious RCU usage,
since a MT_FLAGS_LOCK_EXTERN tree checks the external lock
- explain why the IOVA_DEFERRED store cannot fail, and check it in
test_marker_store_needs_no_node()
- deferred erase is now patch 2, the test suite patch 3
- rebased onto v7.2-rc8
v4:
- reduce the size of struct iova to 16 bytes
- simplify the (hopefully rare) remove_iova GFP_ATOMIC failure path
- test case for the deferred free code
v3:
- switch to maple tree (suggested by Robin Murphy)
v2:
- clean up selftests (thanks Jason Gunthorpe)
- drop the search-with-alignment, since most iova requests should be
of similar sizes, so the worst case behavior is unlikely to hit
once ranges are excluded by the augmented rbtree
base-commit: ad8d485e665829ecbf3c97b22ce251f8ff5f8037
^ permalink raw reply [flat|nested] 4+ messages in thread
* [RFC PATCH 1/3] iommu/iova: convert from rbtree to maple tree
2026-08-18 15:25 [PATCH v5 0/3] iommu/iova: convert from rbtree to maple tree Rik van Riel
@ 2026-08-18 15:25 ` Rik van Riel
2026-08-18 15:25 ` [RFC PATCH 2/3] iommu/iova: defer maple tree erase on GFP_ATOMIC failure Rik van Riel
2026-08-18 15:25 ` [RFC PATCH 3/3] iommu/iova: add KUnit test suite Rik van Riel
2 siblings, 0 replies; 4+ messages in thread
From: Rik van Riel @ 2026-08-18 15:25 UTC (permalink / raw)
To: linux-kernel
Cc: kernel-team, joro, will, robin.murphy, iommu, liam, maple-tree,
linux-mm, ashok.raj, jgg, kyle, Rik van Riel
alloc_iova() looks for free space by walking the rbtree linearly.
On production workloads at Meta, enough CPUs have ended up in that walk
at the same time to trigger soft lockups.
Index the iova ranges in a maple tree instead. Its gap search makes
alloc_iova() O(log n).
__alloc_and_insert_iova_range() asks mas_empty_area_rev() for the
highest free range below limit_pfn. Alignment is handled by rounding
up the allocation size, prioritizing speed over address space waste,
with the thought that many iova requests on a system will be similar
in size, and reuse the same holes.
remove_iova() stores NULL over the entry's range with
mas_store_gfp(&mas, NULL, GFP_ATOMIC). mas_erase() is not usable here:
it retries a failed allocation with GFP_KERNEL, which the irq-safe
iova_lock does not allow.
That store can fail when the tree needs a node to rebalance, and the
entry then stays in the tree. The next patch marks such a range and
retries the erase from iova_drain_deferred().
reserve_iova() widens the requested range to cover every overlapping
entry in iova_merge_overlaps(), stores one merged entry, and frees the
entries that entry supersedes. A request already covered by an existing
entry gets that entry back.
struct iova loses its rb_node and shrinks from 40 to 16 bytes. Remove
SLAB_HWCACHE_ALIGN to pack 4 iova structs in a cache line.
The tree tracks gaps itself, so the IOVA_ANCHOR sentinel and the
cached_node and cached32_node fields are gone.
iova_rbtree_lock is renamed to iova_lock and handed to the tree with
MT_FLAGS_LOCK_EXTERN, because the iova code can be called from interrupt
context, and an irq safe lock is needed. The same lock also protects
related iova data.
The rcache layer is unchanged. It deals in raw pfn values and does not
care what the ranges are kept in.
Assisted-by: Claude:claude-opus-4-8
Suggested-by: Robin Murphy <robin.murphy@arm.com>
Signed-off-by: Rik van Riel <riel@surriel.com>
---
drivers/iommu/iova.c | 379 ++++++++++++++++---------------------------
include/linux/iova.h | 10 +-
2 files changed, 140 insertions(+), 249 deletions(-)
diff --git a/drivers/iommu/iova.c b/drivers/iommu/iova.c
index 021daf6528de..69f80b14cb3d 100644
--- a/drivers/iommu/iova.c
+++ b/drivers/iommu/iova.c
@@ -13,9 +13,7 @@
#include <linux/bitops.h>
#include <linux/cpu.h>
#include <linux/workqueue.h>
-
-/* The anchor node sits above the top of the usable address space */
-#define IOVA_ANCHOR ~0UL
+#include <linux/maple_tree.h>
#define IOVA_RANGE_CACHE_MAX_SIZE 6 /* log of max cached IOVA range size (in pages) */
@@ -29,11 +27,6 @@ static void free_iova_rcaches(struct iova_domain *iovad);
static void free_cpu_cached_iovas(unsigned int cpu, struct iova_domain *iovad);
static void free_global_cached_iovas(struct iova_domain *iovad);
-static struct iova *to_iova(struct rb_node *node)
-{
- return rb_entry(node, struct iova, node);
-}
-
void
init_iova_domain(struct iova_domain *iovad, unsigned long granule,
unsigned long start_pfn)
@@ -45,180 +38,68 @@ init_iova_domain(struct iova_domain *iovad, unsigned long granule,
*/
BUG_ON((granule > PAGE_SIZE) || !is_power_of_2(granule));
- spin_lock_init(&iovad->iova_rbtree_lock);
- iovad->rbroot = RB_ROOT;
- iovad->cached_node = &iovad->anchor.node;
- iovad->cached32_node = &iovad->anchor.node;
+ spin_lock_init(&iovad->iova_lock);
+ /*
+ * IOVAs are freed from hardirq context; use the irq-safe iova_lock,
+ * instead of the mtree lock. It also protects related iova data.
+ */
+ mt_init_flags(&iovad->mtree,
+ MT_FLAGS_ALLOC_RANGE | MT_FLAGS_LOCK_EXTERN);
+ mt_set_external_lock(&iovad->mtree, &iovad->iova_lock);
iovad->granule = granule;
iovad->start_pfn = start_pfn;
iovad->dma_32bit_pfn = 1UL << (32 - iova_shift(iovad));
iovad->max32_alloc_size = iovad->dma_32bit_pfn;
- iovad->anchor.pfn_lo = iovad->anchor.pfn_hi = IOVA_ANCHOR;
- rb_link_node(&iovad->anchor.node, NULL, &iovad->rbroot.rb_node);
- rb_insert_color(&iovad->anchor.node, &iovad->rbroot);
}
EXPORT_SYMBOL_GPL(init_iova_domain);
-static struct rb_node *
-__get_cached_rbnode(struct iova_domain *iovad, unsigned long limit_pfn)
-{
- if (limit_pfn <= iovad->dma_32bit_pfn)
- return iovad->cached32_node;
-
- return iovad->cached_node;
-}
-
-static void
-__cached_rbnode_insert_update(struct iova_domain *iovad, struct iova *new)
-{
- if (new->pfn_hi < iovad->dma_32bit_pfn)
- iovad->cached32_node = &new->node;
- else
- iovad->cached_node = &new->node;
-}
-
-static void
-__cached_rbnode_delete_update(struct iova_domain *iovad, struct iova *free)
-{
- struct iova *cached_iova;
-
- cached_iova = to_iova(iovad->cached32_node);
- if (free == cached_iova ||
- (free->pfn_hi < iovad->dma_32bit_pfn &&
- free->pfn_lo >= cached_iova->pfn_lo))
- iovad->cached32_node = rb_next(&free->node);
-
- if (free->pfn_lo < iovad->dma_32bit_pfn)
- iovad->max32_alloc_size = iovad->dma_32bit_pfn;
-
- cached_iova = to_iova(iovad->cached_node);
- if (free->pfn_lo >= cached_iova->pfn_lo)
- iovad->cached_node = rb_next(&free->node);
-}
-
-static struct rb_node *iova_find_limit(struct iova_domain *iovad, unsigned long limit_pfn)
-{
- struct rb_node *node, *next;
- /*
- * Ideally what we'd like to judge here is whether limit_pfn is close
- * enough to the highest-allocated IOVA that starting the allocation
- * walk from the anchor node will be quicker than this initial work to
- * find an exact starting point (especially if that ends up being the
- * anchor node anyway). This is an incredibly crude approximation which
- * only really helps the most likely case, but is at least trivially easy.
- */
- if (limit_pfn > iovad->dma_32bit_pfn)
- return &iovad->anchor.node;
-
- node = iovad->rbroot.rb_node;
- while (to_iova(node)->pfn_hi < limit_pfn)
- node = node->rb_right;
-
-search_left:
- while (node->rb_left && to_iova(node->rb_left)->pfn_lo >= limit_pfn)
- node = node->rb_left;
-
- if (!node->rb_left)
- return node;
-
- next = node->rb_left;
- while (next->rb_right) {
- next = next->rb_right;
- if (to_iova(next)->pfn_lo >= limit_pfn) {
- node = next;
- goto search_left;
- }
- }
-
- return node;
-}
-
-/* Insert the iova into domain rbtree by holding writer lock */
-static void
-iova_insert_rbtree(struct rb_root *root, struct iova *iova,
- struct rb_node *start)
-{
- struct rb_node **new, *parent = NULL;
-
- new = (start) ? &start : &(root->rb_node);
- /* Figure out where to put new node */
- while (*new) {
- struct iova *this = to_iova(*new);
-
- parent = *new;
-
- if (iova->pfn_lo < this->pfn_lo)
- new = &((*new)->rb_left);
- else if (iova->pfn_lo > this->pfn_lo)
- new = &((*new)->rb_right);
- else {
- WARN_ON(1); /* this should not happen */
- return;
- }
- }
- /* Add new node and rebalance tree. */
- rb_link_node(&iova->node, parent, new);
- rb_insert_color(&iova->node, root);
-}
-
static int __alloc_and_insert_iova_range(struct iova_domain *iovad,
unsigned long size, unsigned long limit_pfn,
struct iova *new, bool size_aligned)
{
- struct rb_node *curr, *prev;
- struct iova *curr_iova;
unsigned long flags;
- unsigned long new_pfn, retry_pfn;
+ unsigned long new_pfn;
unsigned long align_mask = ~0UL;
- unsigned long high_pfn = limit_pfn, low_pfn = iovad->start_pfn;
+ unsigned long search_size = size;
+ MA_STATE(mas, &iovad->mtree, 0, 0);
+
+ if (size_aligned) {
+ unsigned long align = 1UL << fls_long(size - 1);
- if (size_aligned)
align_mask <<= fls_long(size - 1);
+ search_size = size + align - 1;
+ }
- /* Walk the tree backwards */
- spin_lock_irqsave(&iovad->iova_rbtree_lock, flags);
+ spin_lock_irqsave(&iovad->iova_lock, flags);
+ /* No 32-bit request this large can fit until the hint is cleared. */
if (limit_pfn <= iovad->dma_32bit_pfn &&
size >= iovad->max32_alloc_size)
- goto iova32_full;
-
- curr = __get_cached_rbnode(iovad, limit_pfn);
- curr_iova = to_iova(curr);
- retry_pfn = curr_iova->pfn_hi;
-
-retry:
- do {
- high_pfn = min(high_pfn, curr_iova->pfn_lo);
- new_pfn = (high_pfn - size) & align_mask;
- prev = curr;
- curr = rb_prev(curr);
- curr_iova = to_iova(curr);
- } while (curr && new_pfn <= curr_iova->pfn_hi && new_pfn >= low_pfn);
-
- if (high_pfn < size || new_pfn < low_pfn) {
- if (low_pfn == iovad->start_pfn && retry_pfn < limit_pfn) {
- high_pfn = limit_pfn;
- low_pfn = retry_pfn + 1;
- curr = iova_find_limit(iovad, limit_pfn);
- curr_iova = to_iova(curr);
- goto retry;
- }
- iovad->max32_alloc_size = size;
- goto iova32_full;
+ goto alloc_fail;
+
+ if (mas_empty_area_rev(&mas, iovad->start_pfn,
+ limit_pfn - 1, search_size)) {
+ /* Only real exhaustion sets the hint, not a failed store. */
+ if (limit_pfn <= iovad->dma_32bit_pfn)
+ iovad->max32_alloc_size = size;
+ goto alloc_fail;
}
- /* pfn_lo will point to size aligned address if size_aligned is set */
+ /* The gap is search_size wide, so alignment cannot pass start_pfn. */
+ new_pfn = (mas.last - size + 1) & align_mask;
+
new->pfn_lo = new_pfn;
- new->pfn_hi = new->pfn_lo + size - 1;
+ new->pfn_hi = new_pfn + size - 1;
- /* If we have 'prev', it's a valid place to start the insertion. */
- iova_insert_rbtree(&iovad->rbroot, new, prev);
- __cached_rbnode_insert_update(iovad, new);
+ mas.index = new->pfn_lo;
+ mas.last = new->pfn_hi;
+ if (mas_store_gfp(&mas, new, GFP_ATOMIC))
+ goto alloc_fail;
- spin_unlock_irqrestore(&iovad->iova_rbtree_lock, flags);
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
return 0;
-iova32_full:
- spin_unlock_irqrestore(&iovad->iova_rbtree_lock, flags);
+alloc_fail:
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
return -ENOMEM;
}
@@ -233,8 +114,7 @@ static struct iova *alloc_iova_mem(void)
static void free_iova_mem(struct iova *iova)
{
- if (iova->pfn_lo != IOVA_ANCHOR)
- kmem_cache_free(iova_cache, iova);
+ kmem_cache_free(iova_cache, iova);
}
/**
@@ -275,29 +155,29 @@ EXPORT_SYMBOL_GPL(alloc_iova);
static struct iova *
private_find_iova(struct iova_domain *iovad, unsigned long pfn)
{
- struct rb_node *node = iovad->rbroot.rb_node;
-
- assert_spin_locked(&iovad->iova_rbtree_lock);
-
- while (node) {
- struct iova *iova = to_iova(node);
-
- if (pfn < iova->pfn_lo)
- node = node->rb_left;
- else if (pfn > iova->pfn_hi)
- node = node->rb_right;
- else
- return iova; /* pfn falls within iova's range */
- }
+ MA_STATE(mas, &iovad->mtree, pfn, pfn);
- return NULL;
+ assert_spin_locked(&iovad->iova_lock);
+ return mas_walk(&mas);
}
static void remove_iova(struct iova_domain *iovad, struct iova *iova)
{
- assert_spin_locked(&iovad->iova_rbtree_lock);
- __cached_rbnode_delete_update(iovad, iova);
- rb_erase(&iova->node, &iovad->rbroot);
+ MA_STATE(mas, &iovad->mtree, iova->pfn_lo, iova->pfn_hi);
+
+ assert_spin_locked(&iovad->iova_lock);
+
+ if (iova->pfn_lo < iovad->dma_32bit_pfn)
+ iovad->max32_alloc_size = iovad->dma_32bit_pfn;
+
+ /*
+ * A failed store leaves the iova in the tree, so it cannot be freed
+ * here. The range stays reserved until the domain is torn down.
+ */
+ if (mas_store_gfp(&mas, NULL, GFP_ATOMIC))
+ return;
+
+ free_iova_mem(iova);
}
/**
@@ -312,10 +192,9 @@ struct iova *find_iova(struct iova_domain *iovad, unsigned long pfn)
unsigned long flags;
struct iova *iova;
- /* Take the lock so that no other thread is manipulating the rbtree */
- spin_lock_irqsave(&iovad->iova_rbtree_lock, flags);
+ spin_lock_irqsave(&iovad->iova_lock, flags);
iova = private_find_iova(iovad, pfn);
- spin_unlock_irqrestore(&iovad->iova_rbtree_lock, flags);
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
return iova;
}
EXPORT_SYMBOL_GPL(find_iova);
@@ -331,10 +210,9 @@ __free_iova(struct iova_domain *iovad, struct iova *iova)
{
unsigned long flags;
- spin_lock_irqsave(&iovad->iova_rbtree_lock, flags);
+ spin_lock_irqsave(&iovad->iova_lock, flags);
remove_iova(iovad, iova);
- spin_unlock_irqrestore(&iovad->iova_rbtree_lock, flags);
- free_iova_mem(iova);
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
}
EXPORT_SYMBOL_GPL(__free_iova);
@@ -351,15 +229,14 @@ free_iova(struct iova_domain *iovad, unsigned long pfn)
unsigned long flags;
struct iova *iova;
- spin_lock_irqsave(&iovad->iova_rbtree_lock, flags);
+ spin_lock_irqsave(&iovad->iova_lock, flags);
iova = private_find_iova(iovad, pfn);
if (!iova) {
- spin_unlock_irqrestore(&iovad->iova_rbtree_lock, flags);
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
return;
}
remove_iova(iovad, iova);
- spin_unlock_irqrestore(&iovad->iova_rbtree_lock, flags);
- free_iova_mem(iova);
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
}
EXPORT_SYMBOL_GPL(free_iova);
@@ -445,27 +322,26 @@ static void iova_domain_free_rcaches(struct iova_domain *iovad)
*/
void put_iova_domain(struct iova_domain *iovad)
{
- struct iova *iova, *tmp;
+ unsigned long flags;
+ struct iova *iova;
+
+ MA_STATE(mas, &iovad->mtree, 0, 0);
if (iovad->rcaches)
iova_domain_free_rcaches(iovad);
- rbtree_postorder_for_each_entry_safe(iova, tmp, &iovad->rbroot, node)
+ /*
+ * Nothing else can reach a domain being destroyed, but the maple tree
+ * walk still has to hold the lock the tree was given.
+ */
+ spin_lock_irqsave(&iovad->iova_lock, flags);
+ mas_for_each(&mas, iova, ULONG_MAX)
free_iova_mem(iova);
+ __mt_destroy(&iovad->mtree);
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
}
EXPORT_SYMBOL_GPL(put_iova_domain);
-static int
-__is_range_overlap(struct rb_node *node,
- unsigned long pfn_lo, unsigned long pfn_hi)
-{
- struct iova *iova = to_iova(node);
-
- if ((pfn_lo <= iova->pfn_hi) && (pfn_hi >= iova->pfn_lo))
- return 1;
- return 0;
-}
-
static inline struct iova *
alloc_and_init_iova(unsigned long pfn_lo, unsigned long pfn_hi)
{
@@ -480,27 +356,28 @@ alloc_and_init_iova(unsigned long pfn_lo, unsigned long pfn_hi)
return iova;
}
-static struct iova *
-__insert_new_range(struct iova_domain *iovad,
- unsigned long pfn_lo, unsigned long pfn_hi)
+/*
+ * A maple tree cannot hold overlapping ranges, so overlapping reservations
+ * have to be merged into one wider entry.
+ */
+static struct iova *iova_merge_overlaps(struct iova_domain *iovad,
+ unsigned long *lo, unsigned long *hi)
{
- struct iova *iova;
+ unsigned long pfn_lo = *lo, pfn_hi = *hi;
+ struct iova *overlap;
- iova = alloc_and_init_iova(pfn_lo, pfn_hi);
- if (iova)
- iova_insert_rbtree(&iovad->rbroot, iova, NULL);
+ MA_STATE(mas, &iovad->mtree, pfn_lo, pfn_hi);
- return iova;
-}
+ mas_for_each(&mas, overlap, pfn_hi) {
+ if (pfn_lo >= overlap->pfn_lo && pfn_hi <= overlap->pfn_hi)
+ return overlap;
+ if (overlap->pfn_lo < *lo)
+ *lo = overlap->pfn_lo;
+ if (overlap->pfn_hi > *hi)
+ *hi = overlap->pfn_hi;
+ }
-static void
-__adjust_overlap_range(struct iova *iova,
- unsigned long *pfn_lo, unsigned long *pfn_hi)
-{
- if (*pfn_lo < iova->pfn_lo)
- iova->pfn_lo = *pfn_lo;
- if (*pfn_hi > iova->pfn_hi)
- *pfn_lo = iova->pfn_hi + 1;
+ return NULL;
}
/**
@@ -510,41 +387,59 @@ __adjust_overlap_range(struct iova *iova,
* @pfn_hi:- higher pfn address
* This function allocates reserves the address range from pfn_lo to pfn_hi so
* that this address is not dished out as part of alloc_iova.
+ *
+ * If the requested range overlaps existing reservations, ranges are merged.
+ * If the requested range is fully covered by an existing reservation, the
+ * existing entry is returned without allocating.
*/
struct iova *
reserve_iova(struct iova_domain *iovad,
unsigned long pfn_lo, unsigned long pfn_hi)
{
- struct rb_node *node;
+ unsigned long merged_lo = pfn_lo, merged_hi = pfn_hi;
+ struct iova *iova, *overlap;
unsigned long flags;
- struct iova *iova;
- unsigned int overlap = 0;
+
+ MA_STATE(mas, &iovad->mtree, 0, 0);
+ MA_STATE(fmas, &iovad->mtree, 0, 0);
/* Don't allow nonsensical pfns */
if (WARN_ON((pfn_hi | pfn_lo) > (ULLONG_MAX >> iova_shift(iovad))))
return NULL;
- spin_lock_irqsave(&iovad->iova_rbtree_lock, flags);
- for (node = rb_first(&iovad->rbroot); node; node = rb_next(node)) {
- if (__is_range_overlap(node, pfn_lo, pfn_hi)) {
- iova = to_iova(node);
- __adjust_overlap_range(iova, &pfn_lo, &pfn_hi);
- if ((pfn_lo >= iova->pfn_lo) &&
- (pfn_hi <= iova->pfn_hi))
- goto finish;
- overlap = 1;
-
- } else if (overlap)
- break;
- }
+ spin_lock_irqsave(&iovad->iova_lock, flags);
+
+ /*
+ * The overlapping iovas cannot be freed yet: the merged store below
+ * can fail, and freeing before a failed store would leave dangling
+ * pointers in the tree.
+ */
+ iova = iova_merge_overlaps(iovad, &merged_lo, &merged_hi);
+ if (iova)
+ goto out;
+
+ iova = alloc_and_init_iova(merged_lo, merged_hi);
+ if (!iova)
+ goto out;
- /* We are here either because this is the first reserver node
- * or need to insert remaining non overlap addr range
+ /*
+ * The superseded overlaps are freed before the merged store, so the
+ * store must not be able to fail after that point.
*/
- iova = __insert_new_range(iovad, pfn_lo, pfn_hi);
-finish:
+ mas_set_range(&mas, merged_lo, merged_hi);
+ if (mas_preallocate(&mas, iova, GFP_ATOMIC)) {
+ free_iova_mem(iova);
+ iova = NULL;
+ goto out;
+ }
+
+ mas_set_range(&fmas, merged_lo, merged_hi);
+ mas_for_each(&fmas, overlap, merged_hi)
+ free_iova_mem(overlap);
- spin_unlock_irqrestore(&iovad->iova_rbtree_lock, flags);
+ mas_store_prealloc(&mas, iova);
+out:
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
return iova;
}
EXPORT_SYMBOL_GPL(reserve_iova);
@@ -621,7 +516,7 @@ iova_magazine_free_pfns(struct iova_magazine *mag, struct iova_domain *iovad)
unsigned long flags;
int i;
- spin_lock_irqsave(&iovad->iova_rbtree_lock, flags);
+ spin_lock_irqsave(&iovad->iova_lock, flags);
for (i = 0 ; i < mag->size; ++i) {
struct iova *iova = private_find_iova(iovad, mag->pfns[i]);
@@ -633,7 +528,7 @@ iova_magazine_free_pfns(struct iova_magazine *mag, struct iova_domain *iovad)
free_iova_mem(iova);
}
- spin_unlock_irqrestore(&iovad->iova_rbtree_lock, flags);
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
mag->size = 0;
}
@@ -956,8 +851,8 @@ int iova_cache_get(void)
mutex_lock(&iova_cache_mutex);
if (!iova_cache_users) {
- iova_cache = kmem_cache_create("iommu_iova", sizeof(struct iova), 0,
- SLAB_HWCACHE_ALIGN, NULL);
+ iova_cache = kmem_cache_create("iommu_iova", sizeof(struct iova),
+ 0, 0, NULL);
if (!iova_cache)
goto out_err;
diff --git a/include/linux/iova.h b/include/linux/iova.h
index d2c4fd923efa..a3dab6c5fd62 100644
--- a/include/linux/iova.h
+++ b/include/linux/iova.h
@@ -11,12 +11,11 @@
#include <linux/types.h>
#include <linux/kernel.h>
-#include <linux/rbtree.h>
+#include <linux/maple_tree.h>
#include <linux/dma-mapping.h>
/* iova structure */
struct iova {
- struct rb_node node;
unsigned long pfn_hi; /* Highest allocated pfn */
unsigned long pfn_lo; /* Lowest allocated pfn */
};
@@ -26,15 +25,12 @@ struct iova_rcache;
/* holds all the iova translations for a domain */
struct iova_domain {
- spinlock_t iova_rbtree_lock; /* Lock to protect update of rbtree */
- struct rb_root rbroot; /* iova domain rbtree root */
- struct rb_node *cached_node; /* Save last alloced node */
- struct rb_node *cached32_node; /* Save last 32-bit alloced node */
+ spinlock_t iova_lock; /* Protects the maple tree */
+ struct maple_tree mtree;
unsigned long granule; /* pfn granularity for this domain */
unsigned long start_pfn; /* Lower limit for this domain */
unsigned long dma_32bit_pfn;
unsigned long max32_alloc_size; /* Size of last failed allocation */
- struct iova anchor; /* rbtree lookup anchor */
struct iova_rcache *rcaches;
struct hlist_node cpuhp_dead;
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [RFC PATCH 2/3] iommu/iova: defer maple tree erase on GFP_ATOMIC failure
2026-08-18 15:25 [PATCH v5 0/3] iommu/iova: convert from rbtree to maple tree Rik van Riel
2026-08-18 15:25 ` [RFC PATCH 1/3] " Rik van Riel
@ 2026-08-18 15:25 ` Rik van Riel
2026-08-18 15:25 ` [RFC PATCH 3/3] iommu/iova: add KUnit test suite Rik van Riel
2 siblings, 0 replies; 4+ messages in thread
From: Rik van Riel @ 2026-08-18 15:25 UTC (permalink / raw)
To: linux-kernel
Cc: kernel-team, joro, will, robin.murphy, iommu, liam, maple-tree,
linux-mm, ashok.raj, jgg, kyle, Rik van Riel
rb_erase() never allocates. Removing an entry from a maple tree can, so
the mas_store_gfp(&mas, NULL, GFP_ATOMIC) in remove_iova() can fail, and
iovas are freed from atomic context, where GFP_KERNEL is not available.
Failing takes a specific shape. Storing NULL runs mas_wr_extend_null()
first, which pulls adjacent free ranges into the store. With a live iova
on either side there is nothing to pull in, and the store is wr_exact_fit,
which needs no node. Only a free neighbour, dropping the leaf below
mt_min_slots, makes the tree rebalance and allocate.
When the erase does fail, store IOVA_DEFERRED over the entry's own range
instead, and free the struct iova. A non-NULL value does not extend the
range, so that store is wr_exact_fit whatever the neighbours look like,
and cannot fail.
XA_ZERO_ENTRY would reserve the range too, but mtree_load() and mt_find()
turn it back into NULL, so any later use of the normal API on this tree
would see a marked range as free.
The domain keeps a [deferred_lo, deferred_hi] range covering the markers.
iova_drain_deferred() walks only that range, and retries the erases from
the next allocation or the next successful free.
struct iova stays at 16 bytes: the state is the marker plus two unsigned
longs per domain. The store that can fail now runs where failure can be
returned to the caller, so no timer or workqueue is needed.
Should the marker store fail anyway, WARN_ON_ONCE() reports it and the
entry stays in the tree as a live iova. Its range is then reserved until
put_iova_domain() frees it with the rest of the domain. BUG_ON() would
trade that stranded range for a dead machine.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Rik van Riel <riel@surriel.com>
---
drivers/iommu/iova.c | 131 +++++++++++++++++++++++++++++++++++++------
include/linux/iova.h | 3 +
2 files changed, 118 insertions(+), 16 deletions(-)
diff --git a/drivers/iommu/iova.c b/drivers/iommu/iova.c
index 69f80b14cb3d..255a1cefe8c5 100644
--- a/drivers/iommu/iova.c
+++ b/drivers/iommu/iova.c
@@ -26,6 +26,23 @@ static unsigned long iova_rcache_get(struct iova_domain *iovad,
static void free_iova_rcaches(struct iova_domain *iovad);
static void free_cpu_cached_iovas(unsigned int cpu, struct iova_domain *iovad);
static void free_global_cached_iovas(struct iova_domain *iovad);
+static void iova_drain_deferred(struct iova_domain *iovad);
+
+/*
+ * Stored over an entry whose erase failed: an in-place store of a
+ * non-NULL value needs no node allocation, so it works where the
+ * erase did not. The gap search treats the slot as occupied, keeping
+ * the range reserved; lookups, teardown and the invariant checker
+ * skip it. Pointing at a static keeps it 8-byte aligned, out of the
+ * range the maple tree uses for internal entries.
+ */
+static const unsigned long iova_deferred_marker;
+#define IOVA_DEFERRED ((struct iova *)&iova_deferred_marker)
+
+static inline bool iova_has_deferred(const struct iova_domain *iovad)
+{
+ return iovad->deferred_lo <= iovad->deferred_hi;
+}
void
init_iova_domain(struct iova_domain *iovad, unsigned long granule,
@@ -50,6 +67,8 @@ init_iova_domain(struct iova_domain *iovad, unsigned long granule,
iovad->start_pfn = start_pfn;
iovad->dma_32bit_pfn = 1UL << (32 - iova_shift(iovad));
iovad->max32_alloc_size = iovad->dma_32bit_pfn;
+ iovad->deferred_lo = ULONG_MAX;
+ iovad->deferred_hi = 0;
}
EXPORT_SYMBOL_GPL(init_iova_domain);
@@ -71,6 +90,9 @@ static int __alloc_and_insert_iova_range(struct iova_domain *iovad,
}
spin_lock_irqsave(&iovad->iova_lock, flags);
+ /* Reclaim deferred frees to get their address space back. */
+ if (unlikely(iova_has_deferred(iovad)))
+ iova_drain_deferred(iovad);
/* No 32-bit request this large can fit until the hint is cleared. */
if (limit_pfn <= iovad->dma_32bit_pfn &&
size >= iovad->max32_alloc_size)
@@ -156,28 +178,96 @@ static struct iova *
private_find_iova(struct iova_domain *iovad, unsigned long pfn)
{
MA_STATE(mas, &iovad->mtree, pfn, pfn);
+ struct iova *iova;
+
+ assert_spin_locked(&iovad->iova_lock);
+ iova = mas_walk(&mas);
+ /* A deferred-erase marker is not a live iova; treat it as absent. */
+ if (iova == IOVA_DEFERRED)
+ return NULL;
+ return iova;
+}
+
+/* Every marker lies within [deferred_lo, deferred_hi]. Needs iova_lock. */
+static void iova_drain_deferred(struct iova_domain *iovad)
+{
+ unsigned long hi = iovad->deferred_hi;
+ void *entry;
+
+ MA_STATE(mas, &iovad->mtree, iovad->deferred_lo, hi);
assert_spin_locked(&iovad->iova_lock);
- return mas_walk(&mas);
+
+ while ((entry = mas_find(&mas, hi)) != NULL) {
+ unsigned long lo = mas.index, last = mas.last;
+
+ if (entry == IOVA_DEFERRED) {
+ mas_set_range(&mas, lo, last);
+ if (mas_store_gfp(&mas, NULL, GFP_ATOMIC)) {
+ /*
+ * No reclaim can happen under the lock, so
+ * the remaining erases would fail too.
+ */
+ iovad->deferred_lo = lo;
+ iovad->deferred_hi = hi;
+ return;
+ }
+ /* The store moves the iterator; re-anchor. */
+ mas_set(&mas, last + 1);
+ }
+
+ if (last >= hi)
+ break;
+ }
+
+ iovad->deferred_lo = ULONG_MAX;
+ iovad->deferred_hi = 0;
}
+/*
+ * Must not fail: DMA unmap runs in atomic context, so there is no caller to
+ * return an error to.
+ */
static void remove_iova(struct iova_domain *iovad, struct iova *iova)
{
- MA_STATE(mas, &iovad->mtree, iova->pfn_lo, iova->pfn_hi);
+ unsigned long pfn_lo = iova->pfn_lo, pfn_hi = iova->pfn_hi;
+
+ MA_STATE(mas, &iovad->mtree, pfn_lo, pfn_hi);
assert_spin_locked(&iovad->iova_lock);
- if (iova->pfn_lo < iovad->dma_32bit_pfn)
+ if (pfn_lo < iovad->dma_32bit_pfn)
iovad->max32_alloc_size = iovad->dma_32bit_pfn;
- /*
- * A failed store leaves the iova in the tree, so it cannot be freed
- * here. The range stays reserved until the domain is torn down.
- */
- if (mas_store_gfp(&mas, NULL, GFP_ATOMIC))
+ if (mas_store_gfp(&mas, NULL, GFP_ATOMIC)) {
+ /*
+ * A NULL store gets widened over the neighbouring free ranges,
+ * and the wider store may cause a maple tree rebalance, which
+ * can fail if tree nodes failed to allocate. The IOVA_DEFERRED
+ * store is never widened and needs no allocation, as
+ * test_marker_store_needs_no_node() checks. Check anyway,
+ * because surviving e.g. a maple tree corruption here is cheap.
+ */
+ mas_set_range(&mas, pfn_lo, pfn_hi);
+ if (WARN_ON_ONCE(mas_store_gfp(&mas, IOVA_DEFERRED, GFP_ATOMIC)))
+ /*
+ * The tree still holds the live iova: nothing for the
+ * sweep to find, and teardown frees it.
+ */
+ return;
+ if (pfn_lo < iovad->deferred_lo)
+ iovad->deferred_lo = pfn_lo;
+ if (pfn_hi > iovad->deferred_hi)
+ iovad->deferred_hi = pfn_hi;
+ free_iova_mem(iova);
return;
+ }
free_iova_mem(iova);
+
+ /* A successful erase means memory is available; clear any backlog. */
+ if (unlikely(iova_has_deferred(iovad)))
+ iova_drain_deferred(iovad);
}
/**
@@ -231,11 +321,8 @@ free_iova(struct iova_domain *iovad, unsigned long pfn)
spin_lock_irqsave(&iovad->iova_lock, flags);
iova = private_find_iova(iovad, pfn);
- if (!iova) {
- spin_unlock_irqrestore(&iovad->iova_lock, flags);
- return;
- }
- remove_iova(iovad, iova);
+ if (iova)
+ remove_iova(iovad, iova);
spin_unlock_irqrestore(&iovad->iova_lock, flags);
}
EXPORT_SYMBOL_GPL(free_iova);
@@ -335,8 +422,10 @@ void put_iova_domain(struct iova_domain *iovad)
* walk still has to hold the lock the tree was given.
*/
spin_lock_irqsave(&iovad->iova_lock, flags);
+ /* Skip IOVA_DEFERRED entries: their iovas were already freed. */
mas_for_each(&mas, iova, ULONG_MAX)
- free_iova_mem(iova);
+ if (iova != IOVA_DEFERRED)
+ free_iova_mem(iova);
__mt_destroy(&iovad->mtree);
spin_unlock_irqrestore(&iovad->iova_lock, flags);
}
@@ -369,6 +458,12 @@ static struct iova *iova_merge_overlaps(struct iova_domain *iovad,
MA_STATE(mas, &iovad->mtree, pfn_lo, pfn_hi);
mas_for_each(&mas, overlap, pfn_hi) {
+ /*
+ * A deferred-erase marker isn't a real iova; the merged-range
+ * store in the caller spans and overwrites it.
+ */
+ if (overlap == IOVA_DEFERRED)
+ continue;
if (pfn_lo >= overlap->pfn_lo && pfn_hi <= overlap->pfn_hi)
return overlap;
if (overlap->pfn_lo < *lo)
@@ -409,6 +504,10 @@ reserve_iova(struct iova_domain *iovad,
spin_lock_irqsave(&iovad->iova_lock, flags);
+ /* Leave the walk below with only live iovas to look at. */
+ if (unlikely(iova_has_deferred(iovad)))
+ iova_drain_deferred(iovad);
+
/*
* The overlapping iovas cannot be freed yet: the merged store below
* can fail, and freeing before a failed store would leave dangling
@@ -435,7 +534,8 @@ reserve_iova(struct iova_domain *iovad,
mas_set_range(&fmas, merged_lo, merged_hi);
mas_for_each(&fmas, overlap, merged_hi)
- free_iova_mem(overlap);
+ if (overlap != IOVA_DEFERRED)
+ free_iova_mem(overlap);
mas_store_prealloc(&mas, iova);
out:
@@ -525,7 +625,6 @@ iova_magazine_free_pfns(struct iova_magazine *mag, struct iova_domain *iovad)
continue;
remove_iova(iovad, iova);
- free_iova_mem(iova);
}
spin_unlock_irqrestore(&iovad->iova_lock, flags);
diff --git a/include/linux/iova.h b/include/linux/iova.h
index a3dab6c5fd62..e17bb1e85838 100644
--- a/include/linux/iova.h
+++ b/include/linux/iova.h
@@ -31,6 +31,9 @@ struct iova_domain {
unsigned long start_pfn; /* Lower limit for this domain */
unsigned long dma_32bit_pfn;
unsigned long max32_alloc_size; /* Size of last failed allocation */
+ /* Bounding range of deferred erases; lo > hi when there are none. */
+ unsigned long deferred_lo;
+ unsigned long deferred_hi;
struct iova_rcache *rcaches;
struct hlist_node cpuhp_dead;
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [RFC PATCH 3/3] iommu/iova: add KUnit test suite
2026-08-18 15:25 [PATCH v5 0/3] iommu/iova: convert from rbtree to maple tree Rik van Riel
2026-08-18 15:25 ` [RFC PATCH 1/3] " Rik van Riel
2026-08-18 15:25 ` [RFC PATCH 2/3] iommu/iova: defer maple tree erase on GFP_ATOMIC failure Rik van Riel
@ 2026-08-18 15:25 ` Rik van Riel
2 siblings, 0 replies; 4+ messages in thread
From: Rik van Riel @ 2026-08-18 15:25 UTC (permalink / raw)
To: linux-kernel
Cc: kernel-team, joro, will, robin.murphy, iommu, liam, maple-tree,
linux-mm, ashok.raj, jgg, kyle, Rik van Riel
Add a KUnit suite for the maple tree based allocator, plus an
iova_domain_verify_invariants() helper, built only with the test config,
that walks the tree and checks that every entry's pfn_lo and pfn_hi
match its index range and that no two entries overlap.
The cases cover size-aligned allocation across orders 0 to 7, top-down
placement, reserved ranges, a 32-bit allocation in a filled 64-bit
domain, arbitrary limit_pfn values, allocation into a fragmented space,
and the 32-bit-first with 64-bit fallback pattern from dma-iommu.c.
test_stress_random runs 2048 random allocations and frees with mixed
sizes, alignments and DMA limits, checking the invariants after each
operation, from a fixed seed so a failure reproduces across boots.
Two cases fill the space and then time an allocation that has to fail,
which with the rbtree meant walking every entry.
A test hook in remove_iova() forces the GFP_ATOMIC erase to fail, so the
suite can check that the IOVA_DEFERRED marker keeps the range reserved
and that iova_drain_deferred() hands it back.
remove_iova() also relies on that marker store not failing in turn, since
it frees the struct iova while the tree still holds the range.
The guarantee comes from the maple tree. mas_wr_extend_null() widens a
NULL store over the neighbouring free ranges, and the wider store may
need a node. A non-NULL value over the entry's own range is never
widened, so it is an exact fit needing none.
iova_kunit_store_types() reports the store type picked for erasing an
iova and for marking it deferred. test_marker_store_needs_no_node()
drives it over an iova with free neighbours on both sides: erasing is not
an exact fit, marking is, and marking asks for no nodes.
A change to the store-type rules then fails a test rather than reaching a
WARN whose only recovery is to strand an address range.
Run with:
tools/testing/kunit/kunit.py run --kunitconfig=drivers/iommu
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Rik van Riel <riel@surriel.com>
---
drivers/iommu/.kunitconfig | 6 +
drivers/iommu/Kconfig | 16 ++
drivers/iommu/Makefile | 1 +
drivers/iommu/iova-kunit.c | 544 +++++++++++++++++++++++++++++++++++++
drivers/iommu/iova.c | 87 +++++-
include/linux/iova.h | 8 +
6 files changed, 661 insertions(+), 1 deletion(-)
create mode 100644 drivers/iommu/.kunitconfig
create mode 100644 drivers/iommu/iova-kunit.c
diff --git a/drivers/iommu/.kunitconfig b/drivers/iommu/.kunitconfig
new file mode 100644
index 000000000000..d2bb924b1883
--- /dev/null
+++ b/drivers/iommu/.kunitconfig
@@ -0,0 +1,6 @@
+CONFIG_KUNIT=y
+CONFIG_IOMMU_SUPPORT=y
+CONFIG_IOMMU_IOVA=y
+CONFIG_IOMMU_IOVA_KUNIT_TEST=y
+CONFIG_KASAN=y
+CONFIG_KASAN_GENERIC=y
diff --git a/drivers/iommu/Kconfig b/drivers/iommu/Kconfig
index 6e07bd69467a..904e04aeae21 100644
--- a/drivers/iommu/Kconfig
+++ b/drivers/iommu/Kconfig
@@ -3,6 +3,22 @@
config IOMMU_IOVA
tristate
+config IOMMU_IOVA_KUNIT_TEST
+ tristate "KUnit tests for the IOVA allocator" if !KUNIT_ALL_TESTS
+ depends on IOMMU_IOVA && KUNIT
+ default KUNIT_ALL_TESTS
+ help
+ Enable kunit tests for the IOVA allocator. The tests exercise
+ basic allocation and free, size-aligned allocation, top-down
+ ordering, bounded allocations with various DMA limits (32-bit,
+ 33-bit, 56-bit), aligned allocations in fragmented domains,
+ and randomly-fragmented stress scenarios.
+
+ Run with:
+ tools/testing/kunit/kunit.py run --kunitconfig=drivers/iommu
+
+ If unsure, say N here.
+
# IOMMU_API always gets selected by whoever wants it.
config IOMMU_API
bool
diff --git a/drivers/iommu/Makefile b/drivers/iommu/Makefile
index 2f05725eaab1..0b533911f25f 100644
--- a/drivers/iommu/Makefile
+++ b/drivers/iommu/Makefile
@@ -17,6 +17,7 @@ obj-$(CONFIG_IOMMU_IO_PGTABLE_LPAE) += io-pgtable-arm.o
obj-$(CONFIG_IOMMU_IO_PGTABLE_LPAE_KUNIT_TEST) += io-pgtable-arm-selftests.o
obj-$(CONFIG_IOMMU_IO_PGTABLE_DART) += io-pgtable-dart.o
obj-$(CONFIG_IOMMU_IOVA) += iova.o
+obj-$(CONFIG_IOMMU_IOVA_KUNIT_TEST) += iova-kunit.o
obj-$(CONFIG_OF_IOMMU) += of_iommu.o
obj-$(CONFIG_MSM_IOMMU) += msm_iommu.o
obj-$(CONFIG_IPMMU_VMSA) += ipmmu-vmsa.o
diff --git a/drivers/iommu/iova-kunit.c b/drivers/iommu/iova-kunit.c
new file mode 100644
index 000000000000..5c379bf8ac8d
--- /dev/null
+++ b/drivers/iommu/iova-kunit.c
@@ -0,0 +1,544 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * KUnit tests for the IOVA allocator.
+ *
+ * Exercises the maple-tree-based allocator: basic alloc/free,
+ * size-aligned allocations, top-down ordering, bounded allocations
+ * with various DMA limits (32-bit, 33-bit, 56-bit), aligned
+ * allocations in fragmented domains, and randomly fragmented stress.
+ *
+ * Each test verifies that the maple tree invariants remain consistent
+ * after every batch of operations.
+ */
+#include <kunit/test.h>
+#include <linux/dma-mapping.h>
+#include <linux/iova.h>
+
+#define TEST_GRANULE PAGE_SIZE
+/* Highest pfn that fits in 32 bits — triggers the bounded alloc path. */
+#define TEST_LIMIT_32BIT (DMA_BIT_MASK(32) >> PAGE_SHIFT)
+/* 33-bit limit — exercises non-power-of-two DMA boundaries. */
+#define TEST_LIMIT_33BIT (DMA_BIT_MASK(33) >> PAGE_SHIFT)
+/* 56-bit limit — typical server IOMMU address width. */
+#define TEST_LIMIT_56BIT (DMA_BIT_MASK(56) >> PAGE_SHIFT)
+/* A 64-bit-ish limit well above dma_32bit_pfn. 1ULL avoids UB on ILP32. */
+#define TEST_LIMIT_64BIT ((1ULL << 36) >> PAGE_SHIFT)
+/*
+ * A small <=32-bit limit used by tests that want to actually exhaust the
+ * restricted region within a tractable number of allocations.
+ */
+#define TEST_LIMIT_32BIT_RESTRICTED 256UL
+
+struct iova_test_ctx {
+ struct iova_domain iovad;
+ bool initialized;
+};
+
+static int iova_test_init(struct kunit *test)
+{
+ struct iova_test_ctx *ctx;
+ int ret;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ if (!ctx)
+ return -ENOMEM;
+ test->priv = ctx;
+
+ ret = iova_cache_get();
+ if (ret)
+ return ret;
+
+ init_iova_domain(&ctx->iovad, TEST_GRANULE, 1);
+ ret = iova_domain_init_rcaches(&ctx->iovad);
+ if (ret) {
+ put_iova_domain(&ctx->iovad);
+ iova_cache_put();
+ return ret;
+ }
+ ctx->initialized = true;
+
+ KUNIT_ASSERT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+ return 0;
+}
+
+static void iova_test_exit(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+
+ if (ctx && ctx->initialized) {
+ put_iova_domain(&ctx->iovad);
+ ctx->initialized = false;
+ iova_cache_put();
+ }
+}
+
+static void test_size_aligned(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ int order;
+
+ for (order = 0; order < 8; ++order) {
+ unsigned long size = 1UL << order;
+ struct iova *iova = alloc_iova(&ctx->iovad, size,
+ TEST_LIMIT_32BIT, true);
+
+ KUNIT_ASSERT_NOT_NULL(test, iova);
+ KUNIT_EXPECT_EQ(test, iova->pfn_lo & (size - 1), 0);
+ KUNIT_EXPECT_EQ(test, iova->pfn_hi - iova->pfn_lo + 1, size);
+ __free_iova(&ctx->iovad, iova);
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+ }
+}
+
+static void test_top_down_preference(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ struct iova *iovas[16];
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(iovas); ++i) {
+ iovas[i] = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_32BIT, false);
+ KUNIT_ASSERT_NOT_NULL(test, iovas[i]);
+ if (i > 0)
+ KUNIT_EXPECT_LT(test, iovas[i]->pfn_lo,
+ iovas[i - 1]->pfn_lo);
+ }
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+
+ for (i = 0; i < ARRAY_SIZE(iovas); ++i)
+ __free_iova(&ctx->iovad, iovas[i]);
+}
+
+static void test_reserve_iova(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ const unsigned long reserve_lo = TEST_LIMIT_32BIT / 2;
+ struct iova *r, *iova;
+ int i;
+
+ /* Reserve the entire top half through the limit_pfn, inclusive. */
+ r = reserve_iova(&ctx->iovad, reserve_lo, TEST_LIMIT_32BIT);
+ KUNIT_ASSERT_NOT_NULL(test, r);
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+
+ /* All allocs must land below the reserved range. */
+ for (i = 0; i < 100; ++i) {
+ iova = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_32BIT, false);
+ KUNIT_ASSERT_NOT_NULL(test, iova);
+ KUNIT_EXPECT_LT(test, iova->pfn_hi, reserve_lo);
+ }
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+}
+
+/*
+ * The pci_32bit_workaround scenario: every PCI device's first IOVA
+ * allocation hits the 32-bit-restricted path before falling back to
+ * 64-bit. Fill the 64-bit space, then verify a 32-bit alloc still
+ * finds a slot below DMA_BIT_MASK(32).
+ */
+static void test_32bit_in_64bit_domain(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ struct iova *iova;
+ int i;
+
+ for (i = 0; i < 1000; ++i) {
+ iova = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_64BIT, true);
+ KUNIT_ASSERT_NOT_NULL(test, iova);
+ }
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+
+ iova = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_32BIT, true);
+ KUNIT_ASSERT_NOT_NULL(test, iova);
+ KUNIT_EXPECT_LE(test, iova->pfn_hi, TEST_LIMIT_32BIT);
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+
+ __free_iova(&ctx->iovad, iova);
+}
+
+/*
+ * Exercise non-power-of-two DMA limits: fill the 64-bit space, then
+ * verify that bounded allocations at 33-bit and 56-bit limits still
+ * find slots within their respective ranges. This confirms the
+ * navigate-to-limit_pfn search generalizes beyond the 32-bit case.
+ */
+static void test_arbitrary_dma_limits(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ struct iova *iova;
+ int i;
+
+ for (i = 0; i < 1000; ++i) {
+ iova = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_64BIT, true);
+ KUNIT_ASSERT_NOT_NULL(test, iova);
+ }
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+
+ /* 33-bit bounded allocation */
+ iova = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_33BIT, true);
+ KUNIT_ASSERT_NOT_NULL(test, iova);
+ KUNIT_EXPECT_LE(test, iova->pfn_hi, TEST_LIMIT_33BIT);
+ __free_iova(&ctx->iovad, iova);
+
+ /* 56-bit bounded allocation */
+ iova = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_56BIT, true);
+ KUNIT_ASSERT_NOT_NULL(test, iova);
+ KUNIT_EXPECT_LE(test, iova->pfn_hi, TEST_LIMIT_56BIT);
+ __free_iova(&ctx->iovad, iova);
+
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+}
+
+/*
+ * Aligned allocation in a fragmented domain: pack size-2 size_aligned
+ * allocations at the top, free every other one to leave size-2 holes,
+ * then verify a fresh size-2 aligned alloc still succeeds and returns
+ * a 2-aligned pfn.
+ */
+static void test_aligned_in_fragmented(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ const int N = 64;
+ struct iova **iovas;
+ struct iova *iova;
+ int i;
+
+ iovas = kunit_kcalloc(test, N, sizeof(*iovas), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, iovas);
+
+ for (i = 0; i < N; ++i) {
+ iovas[i] = alloc_iova(&ctx->iovad, 2, TEST_LIMIT_32BIT, true);
+ KUNIT_ASSERT_NOT_NULL(test, iovas[i]);
+ KUNIT_EXPECT_EQ(test, iovas[i]->pfn_lo & 1, 0);
+ }
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+
+ for (i = 0; i < N; i += 2) {
+ __free_iova(&ctx->iovad, iovas[i]);
+ iovas[i] = NULL;
+ }
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+
+ iova = alloc_iova(&ctx->iovad, 2, TEST_LIMIT_32BIT, true);
+ KUNIT_ASSERT_NOT_NULL(test, iova);
+ KUNIT_EXPECT_EQ(test, iova->pfn_lo & 1, 0);
+ __free_iova(&ctx->iovad, iova);
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+
+ for (i = 0; i < N; ++i)
+ if (iovas[i])
+ __free_iova(&ctx->iovad, iovas[i]);
+}
+
+/*
+ * Mimic dma-iommu's pci_32bit_workaround pattern: every alloc first
+ * tries a small restricted limit; if that fails, retry with the 64-bit
+ * limit. Verifies that the navigate-to-limit search survives rapid
+ * switching between different limit_pfn values.
+ */
+static void test_pci_32bit_workaround_pattern(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ int fallback_count = 0;
+ int i;
+
+ for (i = 0; i < 500; ++i) {
+ unsigned long size = (i % 4) + 1;
+ struct iova *iova = alloc_iova(&ctx->iovad, size,
+ TEST_LIMIT_32BIT_RESTRICTED,
+ true);
+
+ if (!iova) {
+ iova = alloc_iova(&ctx->iovad, size,
+ TEST_LIMIT_64BIT, true);
+ fallback_count++;
+ }
+ if (!iova)
+ break;
+ }
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+ /* Every alloc must succeed (via fallback once the restricted region fills). */
+ KUNIT_EXPECT_EQ(test, i, 500);
+ /* The restricted region is small, so the 64-bit fallback must engage. */
+ KUNIT_EXPECT_GT(test, fallback_count, 0);
+}
+
+/*
+ * Random alloc/free over many iterations, verifying invariants after
+ * every operation. Uses a deterministic PRNG so failures reproduce
+ * across boots. Exercises mixed DMA limits (32, 33, 56, 64-bit).
+ */
+static void test_stress_random(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ const int N = 512;
+ const int iters = 4 * N;
+ const unsigned long limits[] = {
+ TEST_LIMIT_32BIT, TEST_LIMIT_33BIT,
+ TEST_LIMIT_56BIT, TEST_LIMIT_64BIT,
+ };
+ struct iova **iovas;
+ u32 rng = 0xDEADBEEF;
+ int i;
+
+ iovas = kunit_kcalloc(test, N, sizeof(*iovas), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, iovas);
+
+ for (i = 0; i < iters; ++i) {
+ int slot;
+ unsigned long limit;
+ const char *op;
+
+ rng = rng * 1103515245 + 12345;
+ slot = (rng >> 8) % N;
+ rng = rng * 1103515245 + 12345;
+ limit = limits[(rng >> 8) % ARRAY_SIZE(limits)];
+
+ if (iovas[slot]) {
+ op = "free";
+ __free_iova(&ctx->iovad, iovas[slot]);
+ iovas[slot] = NULL;
+ } else {
+ unsigned long size;
+ bool aligned;
+
+ rng = rng * 1103515245 + 12345;
+ size = 1UL << ((rng >> 8) % 4);
+ rng = rng * 1103515245 + 12345;
+ aligned = (rng >> 8) & 1;
+
+ op = "alloc";
+ iovas[slot] = alloc_iova(&ctx->iovad, size, limit,
+ aligned);
+ }
+ if (!iova_domain_verify_invariants(&ctx->iovad)) {
+ kunit_info(test, "iter %d slot %d: invariant broken after %s\n",
+ i, slot, op);
+ KUNIT_FAIL(test, "verify failed");
+ break;
+ }
+ }
+
+ for (i = 0; i < N; ++i)
+ if (iovas[i])
+ __free_iova(&ctx->iovad, iovas[i]);
+}
+
+/*
+ * Verify that alloc_iova fails in bounded time when the IOVA space is
+ * fully packed. Fill a 16K-pfn range with size-1 allocations (leaving
+ * no gaps), then attempt a size-2 aligned alloc. The maple tree's
+ * mas_empty_area_rev must determine there is no suitable gap in
+ * O(log n) time rather than walking every entry. The wall-clock check
+ * is a loose hang detector only (CI under KASAN/lockdep/virt is slow);
+ * the real signal is the reported time and that the alloc fails.
+ */
+static void test_full_space_search_time(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ const unsigned long fill_limit = 16384;
+ const int fill_count = fill_limit;
+ struct iova *iova;
+ ktime_t start, elapsed;
+ int i, allocated = 0;
+
+ for (i = 0; i < fill_count; ++i) {
+ iova = alloc_iova(&ctx->iovad, 1, fill_limit, false);
+ if (!iova)
+ break;
+ allocated++;
+ }
+ kunit_info(test, "allocated %d iovas in [1, %lu]\n",
+ allocated, fill_limit);
+ KUNIT_ASSERT_GT(test, allocated, 1000);
+
+ start = ktime_get();
+ iova = alloc_iova(&ctx->iovad, 2, fill_limit, true);
+ elapsed = ktime_sub(ktime_get(), start);
+
+ KUNIT_EXPECT_NULL(test, iova);
+ kunit_info(test, "failed alloc took %lld ns\n",
+ ktime_to_ns(elapsed));
+ /* Loose hang detector, not a perf gate (CI under KASAN/lockdep is slow). */
+ KUNIT_EXPECT_LT(test, ktime_to_ns(elapsed), 1000000000LL);
+
+ if (iova)
+ __free_iova(&ctx->iovad, iova);
+}
+
+/*
+ * Verify bounded search time with a fragmented 32-bit IOVA space.
+ * Pack the 32-bit range with size-1 allocs, then attempt a large
+ * aligned alloc that must either succeed from a remaining gap or
+ * fail fast. The 64-bit fallback must always succeed promptly.
+ */
+static void test_fragmented_32bit_search(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ struct iova *iova;
+ ktime_t start, elapsed;
+ int i, allocated = 0;
+
+ for (i = 0; i < 8000; ++i) {
+ iova = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_32BIT, false);
+ if (!iova)
+ break;
+ allocated++;
+ }
+ kunit_info(test, "filled 32-bit space with %d allocs\n", allocated);
+ KUNIT_ASSERT_GT(test, allocated, 1000);
+
+ start = ktime_get();
+ iova = alloc_iova(&ctx->iovad, 32, TEST_LIMIT_32BIT, true);
+ elapsed = ktime_sub(ktime_get(), start);
+
+ kunit_info(test, "32-bit alloc (size 32) took %lld ns, result=%s\n",
+ ktime_to_ns(elapsed), iova ? "alloc" : "fail");
+ /* Loose hang detector, not a perf gate (CI under KASAN/lockdep is slow). */
+ KUNIT_EXPECT_LT(test, ktime_to_ns(elapsed), 1000000000LL);
+
+ if (iova)
+ __free_iova(&ctx->iovad, iova);
+
+ start = ktime_get();
+ iova = alloc_iova(&ctx->iovad, 32, TEST_LIMIT_64BIT, true);
+ elapsed = ktime_sub(ktime_get(), start);
+
+ kunit_info(test, "64-bit fallback (size 32) took %lld ns\n",
+ ktime_to_ns(elapsed));
+ /* Loose hang detector, not a perf gate (CI under KASAN/lockdep is slow). */
+ KUNIT_EXPECT_LT(test, ktime_to_ns(elapsed), 1000000000LL);
+
+ if (iova)
+ __free_iova(&ctx->iovad, iova);
+}
+
+/*
+ * Exercise the deferred-erase path: remove_iova() failing to erase under
+ * GFP_ATOMIC leaves an IOVA_DEFERRED marker in the tree and frees the struct
+ * iova immediately. iova_kunit_defer_erase makes that failure deterministic.
+ * Verify that while marked the range looks free to lookups yet stays reserved,
+ * that invariants hold, and that the next allocation drains the marker and
+ * reuses the space.
+ */
+static void test_deferred_erase(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ struct iova *a, *b;
+ unsigned long pfn;
+
+ a = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_32BIT, false);
+ KUNIT_ASSERT_NOT_NULL(test, a);
+ pfn = a->pfn_lo;
+
+ /* Free 'a', forcing the erase to be deferred (marker left behind). */
+ iova_kunit_defer_erase = true;
+ __free_iova(&ctx->iovad, a);
+ iova_kunit_defer_erase = false;
+
+ /*
+ * The erase was deferred, not performed: a marker now occupies the slot,
+ * so the backlog records the deferral and the pfn looks absent to lookups,
+ * while the tree stays consistent with the marker present.
+ */
+ KUNIT_EXPECT_TRUE(test, iova_domain_has_deferred(&ctx->iovad));
+ KUNIT_EXPECT_NULL(test, find_iova(&ctx->iovad, pfn));
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+
+ /*
+ * The next allocation drains deferred markers before searching, so the
+ * backlog clears and the marked range is reclaimed; a top-down size-1
+ * alloc reuses exactly the pfn that was freed.
+ */
+ b = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_32BIT, false);
+ KUNIT_ASSERT_NOT_NULL(test, b);
+ KUNIT_EXPECT_FALSE(test, iova_domain_has_deferred(&ctx->iovad));
+ KUNIT_EXPECT_EQ(test, b->pfn_lo, pfn);
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+
+ __free_iova(&ctx->iovad, b);
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+}
+
+/*
+ * Tearing down a domain that still holds an undrained IOVA_DEFERRED marker must
+ * skip the marker (it is static storage, not a heap iova) and not crash or
+ * double-free. Leave a marker live for iova_test_exit()'s put_iova_domain().
+ */
+static void test_deferred_erase_teardown(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ struct iova *a;
+
+ a = alloc_iova(&ctx->iovad, 4, TEST_LIMIT_32BIT, false);
+ KUNIT_ASSERT_NOT_NULL(test, a);
+
+ iova_kunit_defer_erase = true;
+ __free_iova(&ctx->iovad, a);
+ iova_kunit_defer_erase = false;
+
+ /* Marker left live; the suite's exit -> put_iova_domain must cope. */
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+}
+
+/*
+ * The deferred-erase path rests on one maple tree property: erasing an entry
+ * next to free space can need a node, while marking that same range with a
+ * non-NULL value cannot. Assert both store types, so a change to the maple
+ * tree's store-type rules is caught here rather than by the WARN_ON_ONCE in
+ * remove_iova(), where the only recovery is to strand the range.
+ */
+static void test_marker_store_needs_no_node(struct kunit *test)
+{
+ struct iova_test_ctx *ctx = test->priv;
+ enum store_type erase, marker;
+ unsigned char marker_nodes;
+ struct iova *a, *b, *c;
+
+ /* Free neighbours on both sides are what make the erase need a node. */
+ c = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_32BIT, false);
+ KUNIT_ASSERT_NOT_NULL(test, c);
+ b = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_32BIT, false);
+ KUNIT_ASSERT_NOT_NULL(test, b);
+ a = alloc_iova(&ctx->iovad, 1, TEST_LIMIT_32BIT, false);
+ KUNIT_ASSERT_NOT_NULL(test, a);
+ __free_iova(&ctx->iovad, a);
+ __free_iova(&ctx->iovad, c);
+
+ iova_kunit_store_types(&ctx->iovad, b, &erase, &marker, &marker_nodes);
+
+ KUNIT_EXPECT_NE(test, erase, wr_exact_fit);
+ KUNIT_EXPECT_EQ(test, marker, wr_exact_fit);
+ KUNIT_EXPECT_EQ(test, marker_nodes, 0);
+
+ __free_iova(&ctx->iovad, b);
+ KUNIT_EXPECT_TRUE(test, iova_domain_verify_invariants(&ctx->iovad));
+}
+
+static struct kunit_case iova_test_cases[] = {
+ KUNIT_CASE(test_size_aligned),
+ KUNIT_CASE(test_top_down_preference),
+ KUNIT_CASE(test_reserve_iova),
+ KUNIT_CASE(test_32bit_in_64bit_domain),
+ KUNIT_CASE(test_arbitrary_dma_limits),
+ KUNIT_CASE(test_aligned_in_fragmented),
+ KUNIT_CASE(test_pci_32bit_workaround_pattern),
+ KUNIT_CASE(test_stress_random),
+ KUNIT_CASE(test_full_space_search_time),
+ KUNIT_CASE(test_fragmented_32bit_search),
+ KUNIT_CASE(test_deferred_erase),
+ KUNIT_CASE(test_deferred_erase_teardown),
+ KUNIT_CASE(test_marker_store_needs_no_node),
+ {}
+};
+
+static struct kunit_suite iova_test_suite = {
+ .name = "iova",
+ .init = iova_test_init,
+ .exit = iova_test_exit,
+ .test_cases = iova_test_cases,
+};
+kunit_test_suite(iova_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for the IOVA allocator");
+MODULE_LICENSE("GPL");
diff --git a/drivers/iommu/iova.c b/drivers/iommu/iova.c
index 255a1cefe8c5..a32413298e29 100644
--- a/drivers/iommu/iova.c
+++ b/drivers/iommu/iova.c
@@ -224,6 +224,47 @@ static void iova_drain_deferred(struct iova_domain *iovad)
iovad->deferred_hi = 0;
}
+#if IS_ENABLED(CONFIG_IOMMU_IOVA_KUNIT_TEST)
+/*
+ * Forces the deferred-erase path, which a real GFP_ATOMIC failure cannot
+ * be provoked into taking on demand.
+ */
+bool iova_kunit_defer_erase;
+EXPORT_SYMBOL_GPL(iova_kunit_defer_erase);
+
+/*
+ * Reports the store type the maple tree picks for erasing @iova and for marking
+ * it deferred. remove_iova() depends on the second being wr_exact_fit, so that
+ * it needs no node; the test asserts on both, and fails if a change to the
+ * store-type rules ever makes the marker store allocate.
+ */
+void iova_kunit_store_types(struct iova_domain *iovad, struct iova *iova,
+ enum store_type *erase, enum store_type *marker,
+ unsigned char *marker_nodes)
+{
+ unsigned long flags;
+
+ MA_STATE(mas, &iovad->mtree, iova->pfn_lo, iova->pfn_hi);
+
+ spin_lock_irqsave(&iovad->iova_lock, flags);
+
+ mas_preallocate(&mas, NULL, GFP_ATOMIC);
+ *erase = mas.store_type;
+ mas_destroy(&mas);
+
+ mas_set_range(&mas, iova->pfn_lo, iova->pfn_hi);
+ mas_preallocate(&mas, IOVA_DEFERRED, GFP_ATOMIC);
+ *marker = mas.store_type;
+ *marker_nodes = mas.node_request;
+ mas_destroy(&mas);
+
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
+}
+EXPORT_SYMBOL_GPL(iova_kunit_store_types);
+#else
+#define iova_kunit_defer_erase false
+#endif
+
/*
* Must not fail: DMA unmap runs in atomic context, so there is no caller to
* return an error to.
@@ -239,7 +280,7 @@ static void remove_iova(struct iova_domain *iovad, struct iova *iova)
if (pfn_lo < iovad->dma_32bit_pfn)
iovad->max32_alloc_size = iovad->dma_32bit_pfn;
- if (mas_store_gfp(&mas, NULL, GFP_ATOMIC)) {
+ if (iova_kunit_defer_erase || mas_store_gfp(&mas, NULL, GFP_ATOMIC)) {
/*
* A NULL store gets widened over the neighbouring free ranges,
* and the wider store may cause a maple tree rebalance, which
@@ -999,6 +1040,50 @@ void iova_cache_put(void)
}
EXPORT_SYMBOL_GPL(iova_cache_put);
+#if IS_ENABLED(CONFIG_IOMMU_IOVA_KUNIT_TEST)
+bool iova_domain_verify_invariants(struct iova_domain *iovad)
+{
+ struct iova *iova, *prev = NULL;
+ unsigned long flags;
+ bool ok = true;
+ MA_STATE(mas, &iovad->mtree, 0, 0);
+
+ spin_lock_irqsave(&iovad->iova_lock, flags);
+ mas_for_each(&mas, iova, ULONG_MAX) {
+ /* A marker occupies a slot but is not a real iova. */
+ if (iova == IOVA_DEFERRED)
+ continue;
+ if (mas.index != iova->pfn_lo || mas.last != iova->pfn_hi) {
+ pr_err("iova_verify: maple index [%lu,%lu] != iova [%lu,%lu]\n",
+ mas.index, mas.last, iova->pfn_lo, iova->pfn_hi);
+ ok = false;
+ }
+ if (iova->pfn_lo > iova->pfn_hi) {
+ pr_err("iova_verify: pfn_lo=%lu > pfn_hi=%lu\n",
+ iova->pfn_lo, iova->pfn_hi);
+ ok = false;
+ }
+ if (prev && prev->pfn_hi >= iova->pfn_lo) {
+ pr_err("iova_verify: overlap prev=[%lu,%lu] curr=[%lu,%lu]\n",
+ prev->pfn_lo, prev->pfn_hi,
+ iova->pfn_lo, iova->pfn_hi);
+ ok = false;
+ }
+ prev = iova;
+ }
+ spin_unlock_irqrestore(&iovad->iova_lock, flags);
+ return ok;
+}
+EXPORT_SYMBOL_GPL(iova_domain_verify_invariants);
+
+/* Test accessor: is there an outstanding deferred-erase backlog? */
+bool iova_domain_has_deferred(struct iova_domain *iovad)
+{
+ return iova_has_deferred(iovad);
+}
+EXPORT_SYMBOL_GPL(iova_domain_has_deferred);
+#endif /* CONFIG_IOMMU_IOVA_KUNIT_TEST */
+
MODULE_AUTHOR("Anil S Keshavamurthy <anil.s.keshavamurthy@intel.com>");
MODULE_DESCRIPTION("IOMMU I/O Virtual Address management");
MODULE_LICENSE("GPL");
diff --git a/include/linux/iova.h b/include/linux/iova.h
index e17bb1e85838..3aef263d0898 100644
--- a/include/linux/iova.h
+++ b/include/linux/iova.h
@@ -101,6 +101,14 @@ void init_iova_domain(struct iova_domain *iovad, unsigned long granule,
int iova_domain_init_rcaches(struct iova_domain *iovad);
struct iova *find_iova(struct iova_domain *iovad, unsigned long pfn);
void put_iova_domain(struct iova_domain *iovad);
+#if IS_ENABLED(CONFIG_IOMMU_IOVA_KUNIT_TEST)
+bool iova_domain_verify_invariants(struct iova_domain *iovad);
+bool iova_domain_has_deferred(struct iova_domain *iovad);
+extern bool iova_kunit_defer_erase;
+void iova_kunit_store_types(struct iova_domain *iovad, struct iova *iova,
+ enum store_type *erase, enum store_type *marker,
+ unsigned char *marker_nodes);
+#endif
#else
static inline int iova_cache_get(void)
{
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-08-18 16:12 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-18 15:25 [PATCH v5 0/3] iommu/iova: convert from rbtree to maple tree Rik van Riel
2026-08-18 15:25 ` [RFC PATCH 1/3] " Rik van Riel
2026-08-18 15:25 ` [RFC PATCH 2/3] iommu/iova: defer maple tree erase on GFP_ATOMIC failure Rik van Riel
2026-08-18 15:25 ` [RFC PATCH 3/3] iommu/iova: add KUnit test suite Rik van Riel
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.