summaryrefslogtreecommitdiffstats
path: root/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-sva.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-sva.c')
-rw-r--r--drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-sva.c244
1 files changed, 242 insertions, 2 deletions
diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-sva.c b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-sva.c
index 9255c9600fb8..e13b092e6004 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-sva.c
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-sva.c
@@ -5,11 +5,35 @@
#include <linux/mm.h>
#include <linux/mmu_context.h>
+#include <linux/mmu_notifier.h>
#include <linux/slab.h>
#include "arm-smmu-v3.h"
+#include "../../iommu-sva-lib.h"
#include "../../io-pgtable-arm.h"
+struct arm_smmu_mmu_notifier {
+ struct mmu_notifier mn;
+ struct arm_smmu_ctx_desc *cd;
+ bool cleared;
+ refcount_t refs;
+ struct list_head list;
+ struct arm_smmu_domain *domain;
+};
+
+#define mn_to_smmu(mn) container_of(mn, struct arm_smmu_mmu_notifier, mn)
+
+struct arm_smmu_bond {
+ struct iommu_sva sva;
+ struct mm_struct *mm;
+ struct arm_smmu_mmu_notifier *smmu_mn;
+ struct list_head list;
+ refcount_t refs;
+};
+
+#define sva_to_bond(handle) \
+ container_of(handle, struct arm_smmu_bond, sva)
+
static DEFINE_MUTEX(sva_lock);
/*
@@ -64,7 +88,6 @@ arm_smmu_share_asid(struct mm_struct *mm, u16 asid)
return NULL;
}
-__maybe_unused
static struct arm_smmu_ctx_desc *arm_smmu_alloc_shared_cd(struct mm_struct *mm)
{
u16 asid;
@@ -145,7 +168,6 @@ out_put_context:
return err < 0 ? ERR_PTR(err) : ret;
}
-__maybe_unused
static void arm_smmu_free_shared_cd(struct arm_smmu_ctx_desc *cd)
{
if (arm_smmu_free_asid(cd)) {
@@ -155,6 +177,215 @@ static void arm_smmu_free_shared_cd(struct arm_smmu_ctx_desc *cd)
}
}
+static void arm_smmu_mm_invalidate_range(struct mmu_notifier *mn,
+ struct mm_struct *mm,
+ unsigned long start, unsigned long end)
+{
+ struct arm_smmu_mmu_notifier *smmu_mn = mn_to_smmu(mn);
+
+ arm_smmu_atc_inv_domain(smmu_mn->domain, mm->pasid, start,
+ end - start + 1);
+}
+
+static void arm_smmu_mm_release(struct mmu_notifier *mn, struct mm_struct *mm)
+{
+ struct arm_smmu_mmu_notifier *smmu_mn = mn_to_smmu(mn);
+ struct arm_smmu_domain *smmu_domain = smmu_mn->domain;
+
+ mutex_lock(&sva_lock);
+ if (smmu_mn->cleared) {
+ mutex_unlock(&sva_lock);
+ return;
+ }
+
+ /*
+ * DMA may still be running. Keep the cd valid to avoid C_BAD_CD events,
+ * but disable translation.
+ */
+ arm_smmu_write_ctx_desc(smmu_domain, mm->pasid, &quiet_cd);
+
+ arm_smmu_tlb_inv_asid(smmu_domain->smmu, smmu_mn->cd->asid);
+ arm_smmu_atc_inv_domain(smmu_domain, mm->pasid, 0, 0);
+
+ smmu_mn->cleared = true;
+ mutex_unlock(&sva_lock);
+}
+
+static void arm_smmu_mmu_notifier_free(struct mmu_notifier *mn)
+{
+ kfree(mn_to_smmu(mn));
+}
+
+static struct mmu_notifier_ops arm_smmu_mmu_notifier_ops = {
+ .invalidate_range = arm_smmu_mm_invalidate_range,
+ .release = arm_smmu_mm_release,
+ .free_notifier = arm_smmu_mmu_notifier_free,
+};
+
+/* Allocate or get existing MMU notifier for this {domain, mm} pair */
+static struct arm_smmu_mmu_notifier *
+arm_smmu_mmu_notifier_get(struct arm_smmu_domain *smmu_domain,
+ struct mm_struct *mm)
+{
+ int ret;
+ struct arm_smmu_ctx_desc *cd;
+ struct arm_smmu_mmu_notifier *smmu_mn;
+
+ list_for_each_entry(smmu_mn, &smmu_domain->mmu_notifiers, list) {
+ if (smmu_mn->mn.mm == mm) {
+ refcount_inc(&smmu_mn->refs);
+ return smmu_mn;
+ }
+ }
+
+ cd = arm_smmu_alloc_shared_cd(mm);
+ if (IS_ERR(cd))
+ return ERR_CAST(cd);
+
+ smmu_mn = kzalloc(sizeof(*smmu_mn), GFP_KERNEL);
+ if (!smmu_mn) {
+ ret = -ENOMEM;
+ goto err_free_cd;
+ }
+
+ refcount_set(&smmu_mn->refs, 1);
+ smmu_mn->cd = cd;
+ smmu_mn->domain = smmu_domain;
+ smmu_mn->mn.ops = &arm_smmu_mmu_notifier_ops;
+
+ ret = mmu_notifier_register(&smmu_mn->mn, mm);
+ if (ret) {
+ kfree(smmu_mn);
+ goto err_free_cd;
+ }
+
+ ret = arm_smmu_write_ctx_desc(smmu_domain, mm->pasid, cd);
+ if (ret)
+ goto err_put_notifier;
+
+ list_add(&smmu_mn->list, &smmu_domain->mmu_notifiers);
+ return smmu_mn;
+
+err_put_notifier:
+ /* Frees smmu_mn */
+ mmu_notifier_put(&smmu_mn->mn);
+err_free_cd:
+ arm_smmu_free_shared_cd(cd);
+ return ERR_PTR(ret);
+}
+
+static void arm_smmu_mmu_notifier_put(struct arm_smmu_mmu_notifier *smmu_mn)
+{
+ struct mm_struct *mm = smmu_mn->mn.mm;
+ struct arm_smmu_ctx_desc *cd = smmu_mn->cd;
+ struct arm_smmu_domain *smmu_domain = smmu_mn->domain;
+
+ if (!refcount_dec_and_test(&smmu_mn->refs))
+ return;
+
+ list_del(&smmu_mn->list);
+ arm_smmu_write_ctx_desc(smmu_domain, mm->pasid, NULL);
+
+ /*
+ * If we went through clear(), we've already invalidated, and no
+ * new TLB entry can have been formed.
+ */
+ if (!smmu_mn->cleared) {
+ arm_smmu_tlb_inv_asid(smmu_domain->smmu, cd->asid);
+ arm_smmu_atc_inv_domain(smmu_domain, mm->pasid, 0, 0);
+ }
+
+ /* Frees smmu_mn */
+ mmu_notifier_put(&smmu_mn->mn);
+ arm_smmu_free_shared_cd(cd);
+}
+
+static struct iommu_sva *
+__arm_smmu_sva_bind(struct device *dev, struct mm_struct *mm)
+{
+ int ret;
+ struct arm_smmu_bond *bond;
+ struct arm_smmu_master *master = dev_iommu_priv_get(dev);
+ struct iommu_domain *domain = iommu_get_domain_for_dev(dev);
+ struct arm_smmu_domain *smmu_domain = to_smmu_domain(domain);
+
+ if (!master || !master->sva_enabled)
+ return ERR_PTR(-ENODEV);
+
+ /* If bind() was already called for this {dev, mm} pair, reuse it. */
+ list_for_each_entry(bond, &master->bonds, list) {
+ if (bond->mm == mm) {
+ refcount_inc(&bond->refs);
+ return &bond->sva;
+ }
+ }
+
+ bond = kzalloc(sizeof(*bond), GFP_KERNEL);
+ if (!bond)
+ return ERR_PTR(-ENOMEM);
+
+ /* Allocate a PASID for this mm if necessary */
+ ret = iommu_sva_alloc_pasid(mm, 1, (1U << master->ssid_bits) - 1);
+ if (ret)
+ goto err_free_bond;
+
+ bond->mm = mm;
+ bond->sva.dev = dev;
+ refcount_set(&bond->refs, 1);
+
+ bond->smmu_mn = arm_smmu_mmu_notifier_get(smmu_domain, mm);
+ if (IS_ERR(bond->smmu_mn)) {
+ ret = PTR_ERR(bond->smmu_mn);
+ goto err_free_pasid;
+ }
+
+ list_add(&bond->list, &master->bonds);
+ return &bond->sva;
+
+err_free_pasid:
+ iommu_sva_free_pasid(mm);
+err_free_bond:
+ kfree(bond);
+ return ERR_PTR(ret);
+}
+
+struct iommu_sva *
+arm_smmu_sva_bind(struct device *dev, struct mm_struct *mm, void *drvdata)
+{
+ struct iommu_sva *handle;
+ struct iommu_domain *domain = iommu_get_domain_for_dev(dev);
+ struct arm_smmu_domain *smmu_domain = to_smmu_domain(domain);
+
+ if (smmu_domain->stage != ARM_SMMU_DOMAIN_S1)
+ return ERR_PTR(-EINVAL);
+
+ mutex_lock(&sva_lock);
+ handle = __arm_smmu_sva_bind(dev, mm);
+ mutex_unlock(&sva_lock);
+ return handle;
+}
+
+void arm_smmu_sva_unbind(struct iommu_sva *handle)
+{
+ struct arm_smmu_bond *bond = sva_to_bond(handle);
+
+ mutex_lock(&sva_lock);
+ if (refcount_dec_and_test(&bond->refs)) {
+ list_del(&bond->list);
+ arm_smmu_mmu_notifier_put(bond->smmu_mn);
+ iommu_sva_free_pasid(bond->mm);
+ kfree(bond);
+ }
+ mutex_unlock(&sva_lock);
+}
+
+u32 arm_smmu_sva_get_pasid(struct iommu_sva *handle)
+{
+ struct arm_smmu_bond *bond = sva_to_bond(handle);
+
+ return bond->mm->pasid;
+}
+
bool arm_smmu_sva_supported(struct arm_smmu_device *smmu)
{
unsigned long reg, fld;
@@ -246,3 +477,12 @@ int arm_smmu_master_disable_sva(struct arm_smmu_master *master)
return 0;
}
+
+void arm_smmu_sva_notifier_synchronize(void)
+{
+ /*
+ * Some MMU notifiers may still be waiting to be freed, using
+ * arm_smmu_mmu_notifier_free(). Wait for them.
+ */
+ mmu_notifier_synchronize();
+}