From: fangyu.yu@linux.alibaba.com
To: tomasz.jeznach@linux.dev, joro@8bytes.org, will@kernel.org,
robin.murphy@arm.com, pjw@kernel.org, palmer@dabbelt.com,
aou@eecs.berkeley.edu, alex@ghiti.fr, jroedel@suse.de,
zong.li@sifive.com, andrew.jones@oss.qualcomm.com,
jgg@nvidia.com, jgg@ziepe.ca, gsh517025@gmail.com
Cc: fangyu.yu@linux.alibaba.com, guoren@kernel.org,
iommu@lists.linux.dev, linux-kernel@vger.kernel.org,
linux-riscv@lists.infradead.org, kvm-riscv@lists.infradead.org
Subject: [RFC PATCH v2 1/4] iommupt: Add replace_leaf to atomically swap a leaf OA
Date: Wed, 14 Oct 2026 22:11:21 +0800 [thread overview]
Message-ID: <20261014141124.48569-2-fangyu.yu@linux.alibaba.com> (raw)
In-Reply-To: <20261014141124.48569-1-fangyu.yu@linux.alibaba.com>
From: Fangyu Yu <fangyu.yu@linux.alibaba.com>
Some users need to repoint a leaf entry to a different physical page
without ever making the IOVA non-present. RISC-V IRQ forwarding without
an MSI page table maps the guest IMSIC page in the second-stage domain
and must follow the page when a vCPU's VS-file host page moves; an
unmap/map sequence would leave a window where device MSI writes fault.
Add a pt_entry_replace_oa() format op that atomically swaps the OA
stored in a leaf entry that fits in a single PTE word via cmpxchg,
validating that the entry still holds the expected old OA, and a
pt_iommu_riscv_64_replace_leaf() export that walks to the leaf. Formats
that cannot do this atomically do not provide the op, following the
pt_iommu_fmt_hw_info() pattern. Covered by a kunit case checking the
granule and superpage swaps, stale OA rejection, and missing
translation.
Signed-off-by: Fangyu Yu <fangyu.yu@linux.alibaba.com>
---
drivers/iommu/generic_pt/fmt/riscv.h | 47 ++++++++++++++++
drivers/iommu/generic_pt/iommu_pt.h | 65 +++++++++++++++++++++++
drivers/iommu/generic_pt/kunit_iommu.h | 7 +++
drivers/iommu/generic_pt/kunit_iommu_pt.h | 54 +++++++++++++++++++
include/linux/generic_pt/iommu.h | 4 ++
5 files changed, 177 insertions(+)
diff --git a/drivers/iommu/generic_pt/fmt/riscv.h b/drivers/iommu/generic_pt/fmt/riscv.h
index 2f4838eb44af..38c837805df0 100644
--- a/drivers/iommu/generic_pt/fmt/riscv.h
+++ b/drivers/iommu/generic_pt/fmt/riscv.h
@@ -27,6 +27,7 @@
#include <linux/bitfield.h>
#include <linux/container_of.h>
+#include <linux/errno.h>
#include <linux/log2.h>
#include <linux/sizes.h>
@@ -272,6 +273,52 @@ static inline bool riscvpt_dirty_supported(struct pt_common *common)
}
#define pt_dirty_supported riscvpt_dirty_supported
+/**
+ * pt_entry_replace_oa() - Atomically replace the OA stored in a leaf
+ * @pts: Table entry to change
+ * @old_oa: OA the entry must currently translate to
+ * @new_oa: OA to store in the entry
+ *
+ * Atomically swap the OA stored in a single PTE word leaf entry, keeping
+ * every other bit of the entry unchanged. The entry must currently translate
+ * to @old_oa so a stale view of the table is detectable. Both @old_oa and
+ * @new_oa must be aligned to the size the entry translates. Entries that do
+ * not fit in a single PTE word, such as contiguous entries, are rejected.
+ *
+ * Return: 0 on success, -EAGAIN if the entry changed concurrently, -EADDRINUSE
+ * if the entry does not translate to @old_oa, -EOPNOTSUPP if the entry spans
+ * multiple PTE words, -EINVAL if an argument is malformed.
+ */
+static inline int riscvpt_entry_replace_oa(struct pt_state *pts,
+ pt_oaddr_t old_oa,
+ pt_oaddr_t new_oa)
+{
+ pt_riscv_entry_t *tablep =
+ pt_cur_table(pts, pt_riscv_entry_t) + pts->index;
+ pt_riscv_entry_t old = pts->entry, new;
+ unsigned int oasz_lg2 = pt_table_item_lg2sz(pts);
+ u64 ppn = (u64)new_oa >> PT_GRANULE_LG2SZ;
+
+ /* Only a leaf entry that fits in a single PTE word can be replaced. */
+ if (pt_entry_num_contig_lg2(pts))
+ return -EOPNOTSUPP;
+
+ if (oalog2_mod(old_oa, oasz_lg2) || oalog2_mod(new_oa, oasz_lg2) ||
+ ppn > FIELD_MAX(RISCVPT_PPN))
+ return -EINVAL;
+
+ /* The leaf must still hold the OA the caller expects to replace. */
+ if (pt_entry_oa(pts) != old_oa)
+ return -EADDRINUSE;
+
+ new = (old & ~(pt_riscv_entry_t)RISCVPT_PPN) |
+ FIELD_PREP(RISCVPT_PPN, ppn);
+ if (!try_cmpxchg64(tablep, &old, new))
+ return -EAGAIN;
+ return 0;
+}
+#define pt_entry_replace_oa riscvpt_entry_replace_oa
+
/* --- iommu */
#include <linux/generic_pt/iommu.h>
#include <linux/iommu.h>
diff --git a/drivers/iommu/generic_pt/iommu_pt.h b/drivers/iommu/generic_pt/iommu_pt.h
index 3d1fdf1e81d8..5e296992c4df 100644
--- a/drivers/iommu/generic_pt/iommu_pt.h
+++ b/drivers/iommu/generic_pt/iommu_pt.h
@@ -375,6 +375,71 @@ static int __maybe_unused NS(set_dirty)(struct pt_iommu *iommu_table,
return pt_walk_range(&range, __set_dirty, NULL);
}
+#ifdef pt_entry_replace_oa
+struct pt_replace_leaf_args {
+ pt_oaddr_t old_oa;
+ pt_oaddr_t new_oa;
+};
+
+static int __replace_leaf(struct pt_range *range, void *arg,
+ unsigned int level, struct pt_table_p *table)
+{
+ struct pt_state pts = pt_init(range, level, table);
+ struct pt_replace_leaf_args *args = arg;
+
+ switch (pt_load_single_entry(&pts)) {
+ case PT_ENTRY_EMPTY:
+ return -ENOENT;
+ case PT_ENTRY_TABLE:
+ return pt_descend(&pts, arg, __replace_leaf);
+ case PT_ENTRY_OA:
+ return pt_entry_replace_oa(&pts, args->old_oa, args->new_oa);
+ }
+ return -ENOENT;
+}
+
+/**
+ * replace_leaf() - Atomically replace the OA stored in a leaf entry
+ * @domain: Table to manipulate
+ * @iova: IO virtual address to locate the leaf
+ * @old_oa: OA the leaf must currently translate to
+ * @new_oa: OA to store in the leaf
+ *
+ * Locate the leaf entry translating @iova and atomically swap its stored OA
+ * from @old_oa to @new_oa, leaving the entry attributes unchanged. HW
+ * walking the table concurrently observes either the old or the new OA. The
+ * caller must invalidate any translation caches before assuming HW no longer
+ * observes the old OA.
+ *
+ * Context: There is no internal locking. The caller must serialize this with
+ * any concurrent map/unmap touching @iova, as with map_range().
+ *
+ * Returns: 0 on success. -EAGAIN indicates the entry changed concurrently,
+ * retry. -ENOENT if @iova has no translation. -EADDRINUSE if the leaf does
+ * not translate to @old_oa. -EOPNOTSUPP if the leaf does not fit in a single
+ * PTE word, such as a contiguous entry.
+ */
+int DOMAIN_NS(replace_leaf)(struct iommu_domain *domain, dma_addr_t iova,
+ phys_addr_t old_oa, phys_addr_t new_oa)
+{
+ struct pt_iommu *iommu_table =
+ container_of(domain, struct pt_iommu, domain);
+ struct pt_replace_leaf_args args = {
+ .old_oa = old_oa,
+ .new_oa = new_oa,
+ };
+ struct pt_range range;
+ int ret;
+
+ ret = make_range(common_from_iommu(iommu_table), &range, iova, 1);
+ if (ret)
+ return ret;
+
+ return pt_walk_range(&range, __replace_leaf, &args);
+}
+EXPORT_SYMBOL_NS_GPL(DOMAIN_NS(replace_leaf), "GENERIC_PT_IOMMU");
+#endif
+
struct pt_iommu_collect_args {
struct iommupt_pending_gather pending;
/* Fail if any OAs are within the range */
diff --git a/drivers/iommu/generic_pt/kunit_iommu.h b/drivers/iommu/generic_pt/kunit_iommu.h
index 22c9e4c4dd97..d52b82c9942c 100644
--- a/drivers/iommu/generic_pt/kunit_iommu.h
+++ b/drivers/iommu/generic_pt/kunit_iommu.h
@@ -16,6 +16,13 @@
int pt_iommu_init(struct pt_iommu_table *fmt_table,
const struct pt_iommu_table_cfg *cfg, gfp_t gfp);
+#ifdef pt_entry_replace_oa
+#define pt_iommu_replace_leaf \
+ CONCATENATE(CONCATENATE(pt_iommu_, PTPFX), replace_leaf)
+int pt_iommu_replace_leaf(struct iommu_domain *domain, dma_addr_t iova,
+ phys_addr_t old_oa, phys_addr_t new_oa);
+#endif
+
/* The format can provide a list of configurations it would like to test */
#ifdef kunit_fmt_cfgs
static const void *kunit_pt_gen_params_cfg(struct kunit *test, const void *prev,
diff --git a/drivers/iommu/generic_pt/kunit_iommu_pt.h b/drivers/iommu/generic_pt/kunit_iommu_pt.h
index ece1c9b8c55d..8707d55418d3 100644
--- a/drivers/iommu/generic_pt/kunit_iommu_pt.h
+++ b/drivers/iommu/generic_pt/kunit_iommu_pt.h
@@ -427,6 +427,57 @@ static void test_mixed(struct kunit *test)
check_iova(test, start, oa, len);
}
+#ifdef pt_entry_replace_oa
+static void test_replace_leaf(struct kunit *test)
+{
+ struct kunit_iommu_priv *priv = test->priv;
+ struct pt_range range = pt_top_range(priv->common);
+ pt_vaddr_t va = range.va + priv->smallest_pgsz * 256;
+ phys_addr_t pa = log2_set_mod(priv->test_oa, 0,
+ priv->smallest_pgsz_lg2);
+ phys_addr_t new_pa = log2_set_mod(priv->test_oa + priv->smallest_pgsz,
+ 0, priv->smallest_pgsz_lg2);
+ int ret;
+
+ do_map(test, va, pa, priv->smallest_pgsz);
+
+ /* The leaf OA can be swapped in place */
+ ret = pt_iommu_replace_leaf(&priv->domain, va, pa, new_pa);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ check_iova(test, va, new_pa, priv->smallest_pgsz);
+
+ /* A stale old OA is detected */
+ ret = pt_iommu_replace_leaf(&priv->domain, va, pa, pa);
+ KUNIT_ASSERT_EQ(test, ret, -EADDRINUSE);
+
+ /* A not-present IOVA reports -ENOENT */
+ ret = pt_iommu_replace_leaf(&priv->domain, va + priv->smallest_pgsz,
+ pa, new_pa);
+ KUNIT_ASSERT_EQ(test, ret, -ENOENT);
+
+ do_unmap(test, va, priv->smallest_pgsz);
+
+ if (!(priv->safe_pgsize_bitmap & SZ_2M))
+ return;
+
+ /* A leaf at a higher level can be swapped in place too */
+ va = ALIGN(va, SZ_2M);
+ pa = log2_set_mod(priv->test_oa, 0, ilog2(SZ_2M));
+ new_pa = log2_set_mod(priv->test_oa + SZ_2M, 0, ilog2(SZ_2M));
+ do_map(test, va, pa, SZ_2M);
+
+ ret = pt_iommu_replace_leaf(&priv->domain, va, pa, new_pa);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ check_iova(test, va, new_pa, SZ_2M);
+
+ /* A stale old OA is detected */
+ ret = pt_iommu_replace_leaf(&priv->domain, va, pa, pa);
+ KUNIT_ASSERT_EQ(test, ret, -EADDRINUSE);
+
+ do_unmap(test, va, SZ_2M);
+}
+#endif
+
static struct kunit_case iommu_test_cases[] = {
KUNIT_CASE_FMT(test_increase_level),
KUNIT_CASE_FMT(test_map_simple),
@@ -435,6 +486,9 @@ static struct kunit_case iommu_test_cases[] = {
KUNIT_CASE_FMT(test_random_map),
KUNIT_CASE_FMT(test_pgsize_boundary),
KUNIT_CASE_FMT(test_mixed),
+#ifdef pt_entry_replace_oa
+ KUNIT_CASE_FMT(test_replace_leaf),
+#endif
{},
};
diff --git a/include/linux/generic_pt/iommu.h b/include/linux/generic_pt/iommu.h
index 2eed9b78f56c..e9042b509013 100644
--- a/include/linux/generic_pt/iommu.h
+++ b/include/linux/generic_pt/iommu.h
@@ -254,6 +254,10 @@ struct pt_iommu_cfg {
int pt_iommu_##fmt##_read_and_clear_dirty( \
struct iommu_domain *domain, unsigned long iova, size_t size, \
unsigned long flags, struct iommu_dirty_bitmap *dirty); \
+ /* Only defined by formats whose fmt header has pt_entry_replace_oa */ \
+ int pt_iommu_##fmt##_replace_leaf(struct iommu_domain *domain, \
+ dma_addr_t iova, phys_addr_t old_oa, \
+ phys_addr_t new_oa); \
int pt_iommu_##fmt##_init(struct pt_iommu_##fmt *table, \
const struct pt_iommu_##fmt##_cfg *cfg, \
gfp_t gfp); \
--
2.50.1
next prev parent reply other threads:[~2026-10-14 14:11 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-14 14:11 [RFC PATCH v2 0/4] iommu/riscv: Add irqbypass support without MSI page table fangyu.yu
2026-10-14 14:11 ` fangyu.yu [this message]
2026-10-14 14:11 ` [RFC PATCH v2 2/4] iommu/riscv: Extract IRQ forwarding payload validation fangyu.yu
2026-10-14 14:11 ` [RFC PATCH v2 3/4] iommu/riscv: Add guest IMSIC GPA mapping helpers fangyu.yu
2026-10-14 14:11 ` [RFC PATCH v2 4/4] iommu/riscv: Support IRQ forwarding without MSI page tables fangyu.yu
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261014141124.48569-2-fangyu.yu@linux.alibaba.com \
--to=fangyu.yu@linux.alibaba.com \
--cc=alex@ghiti.fr \
--cc=andrew.jones@oss.qualcomm.com \
--cc=aou@eecs.berkeley.edu \
--cc=gsh517025@gmail.com \
--cc=guoren@kernel.org \
--cc=iommu@lists.linux.dev \
--cc=jgg@nvidia.com \
--cc=jgg@ziepe.ca \
--cc=joro@8bytes.org \
--cc=jroedel@suse.de \
--cc=kvm-riscv@lists.infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-riscv@lists.infradead.org \
--cc=palmer@dabbelt.com \
--cc=pjw@kernel.org \
--cc=robin.murphy@arm.com \
--cc=tomasz.jeznach@linux.dev \
--cc=will@kernel.org \
--cc=zong.li@sifive.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®