The SMMU only has one ASID space, so the task allocator competes with the
domain allocator for ASIDs. Task ASIDs are shared with CPUs, whereas
domain ASIDs are private to the SMMU, and not affected by broadcast TLB
invalidations. When the task allocator pins a mm_context and gets an ASID
already used by the SMMU, it belongs to a domain. Attempt to assign a new
ASID to the domain, and steal the old one for our shared context.

Replacing an ASID requires some pretty invasive introspection. We could
try to do fine-grained locking on asid_map, domains list, context
descriptors and domains themselves, but it gets terribly complicated and
my brain melted twice before I could find solutions to all lock
dependencies.

Instead, introduce a big fat mutex around domains and (default) contexts
modifications. It ensures that arm_smmu_context_share finds the domain
that owns the ASID we want, and then changes all associated stream table
entries without racing with attach/detach_dev. Note that domain_free is
called after devices have been removed from the group, so
arm_smmu_context_share might do the whole ASID replacement dance for
nothing, but it is harmless.

Signed-off-by: Jean-Philippe Brucker <jean-philippe.bruc...@arm.com>
---
 drivers/iommu/arm-smmu-v3.c | 98 +++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 94 insertions(+), 4 deletions(-)

diff --git a/drivers/iommu/arm-smmu-v3.c b/drivers/iommu/arm-smmu-v3.c
index c3fa4616bd58..3af47b1427a6 100644
--- a/drivers/iommu/arm-smmu-v3.c
+++ b/drivers/iommu/arm-smmu-v3.c
@@ -708,6 +708,9 @@ struct arm_smmu_device {
        spinlock_t                      contexts_lock;
        struct rb_root                  streams;
        struct list_head                tasks;
+
+       struct list_head                domains;
+       struct mutex                    domains_mutex;
 };
 
 struct arm_smmu_stream {
@@ -752,6 +755,8 @@ struct arm_smmu_domain {
 
        struct list_head                groups;
        spinlock_t                      groups_lock;
+
+       struct list_head                list; /* For domain search by ASID */
 };
 
 struct arm_smmu_task {
@@ -2179,11 +2184,79 @@ static const struct mmu_notifier_ops 
arm_smmu_mmu_notifier_ops = {
 static int arm_smmu_context_share(struct arm_smmu_task *smmu_task, int asid)
 {
        int ret = 0;
+       int new_asid;
+       unsigned long flags;
+       struct arm_smmu_group *smmu_group;
+       struct arm_smmu_master_data *master;
        struct arm_smmu_device *smmu = smmu_task->smmu;
+       struct arm_smmu_domain *tmp_domain, *smmu_domain = NULL;
+       struct arm_smmu_cmdq_ent cmd = {
+               .opcode = smmu->features & ARM_SMMU_FEAT_E2H ?
+                         CMDQ_OP_TLBI_EL2_ASID : CMDQ_OP_TLBI_NH_ASID,
+       };
+
+       mutex_lock(&smmu->domains_mutex);
+
+       if (!test_and_set_bit(asid, smmu->asid_map))
+               goto out_unlock;
+
+       /* ASID is used by a domain. Try to replace it with a new one. */
+       new_asid = arm_smmu_bitmap_alloc(smmu->asid_map, smmu->asid_bits);
+       if (new_asid < 0) {
+               ret = new_asid;
+               goto out_unlock;
+       }
+
+       list_for_each_entry(tmp_domain, &smmu->domains, list) {
+               if (tmp_domain->stage != ARM_SMMU_DOMAIN_S1 ||
+                   tmp_domain->s1_cfg.asid != asid)
+                       continue;
+
+               smmu_domain = tmp_domain;
+               break;
+       }
+
+       /*
+        * We didn't find the domain that owns this ASID. It is a bug, since we
+        * hold domains_mutex
+        */
+       if (WARN_ON(!smmu_domain)) {
+               ret = -ENOSPC;
+               goto out_unlock;
+       }
+
+       /*
+        * Race with smmu_unmap; TLB invalidations will start targeting the
+        * new ASID, which isn't assigned yet. We'll do an invalidate-all on
+        * the old ASID later, so it doesn't matter.
+        */
+       smmu_domain->s1_cfg.asid = new_asid;
 
-       if (test_and_set_bit(asid, smmu->asid_map))
-               /* ASID is already used for a domain */
-               return -EEXIST;
+       /*
+        * Update ASID and invalidate CD in all associated masters. There will
+        * be some overlapping between use of both ASIDs, until we invalidate
+        * the TLB.
+        */
+       spin_lock_irqsave(&smmu_domain->groups_lock, flags);
+
+       list_for_each_entry(smmu_group, &smmu_domain->groups, domain_head) {
+               spin_lock(&smmu_group->devices_lock);
+               list_for_each_entry(master, &smmu_group->devices, group_head) {
+                       arm_smmu_write_ctx_desc(master, 0, 
&smmu_domain->s1_cfg);
+               }
+               spin_unlock(&smmu_group->devices_lock);
+       }
+
+       spin_unlock_irqrestore(&smmu_domain->groups_lock, flags);
+
+       /* Invalidate TLB entries previously associated with that domain */
+       cmd.tlbi.asid = asid;
+       arm_smmu_cmdq_issue_cmd(smmu, &cmd);
+       cmd.opcode = CMDQ_OP_CMD_SYNC;
+       arm_smmu_cmdq_issue_cmd(smmu, &cmd);
+
+out_unlock:
+       mutex_unlock(&smmu->domains_mutex);
 
        return ret;
 }
@@ -2426,16 +2499,23 @@ static void arm_smmu_domain_free(struct iommu_domain 
*domain)
        iommu_put_dma_cookie(domain);
        free_io_pgtable_ops(smmu_domain->pgtbl_ops);
 
+       mutex_lock(&smmu->domains_mutex);
+
        if (smmu_domain->stage == ARM_SMMU_DOMAIN_S1) {
                struct arm_smmu_s1_cfg *cfg = &smmu_domain->s1_cfg;
-               if (cfg->asid)
+               if (cfg->asid) {
                        arm_smmu_bitmap_free(smmu->asid_map, cfg->asid);
+
+                       list_del(&smmu_domain->list);
+               }
        } else {
                struct arm_smmu_s2_cfg *cfg = &smmu_domain->s2_cfg;
                if (cfg->vmid)
                        arm_smmu_bitmap_free(smmu->vmid_map, cfg->vmid);
        }
 
+       mutex_unlock(&smmu->domains_mutex);
+
        kfree(smmu_domain);
 }
 
@@ -2455,6 +2535,8 @@ static int arm_smmu_domain_finalise_s1(struct 
arm_smmu_domain *smmu_domain,
        cfg->tcr        = pgtbl_cfg->arm_lpae_s1_cfg.tcr;
        cfg->mair       = pgtbl_cfg->arm_lpae_s1_cfg.mair[0];
 
+       list_add(&smmu_domain->list, &smmu->domains);
+
        return 0;
 }
 
@@ -2604,12 +2686,16 @@ static void arm_smmu_detach_dev(struct device *dev)
        struct arm_smmu_context *smmu_context;
        struct rb_node *node, *next;
 
+       mutex_lock(&smmu->domains_mutex);
+
        master->ste.bypass = true;
        if (arm_smmu_install_ste_for_dev(dev->iommu_fwspec) < 0)
                dev_warn(dev, "failed to install bypass STE\n");
 
        arm_smmu_write_ctx_desc(master, 0, NULL);
 
+       mutex_unlock(&smmu->domains_mutex);
+
        if (!master->ste.valid)
                return;
 
@@ -2682,6 +2768,7 @@ static int arm_smmu_attach_dev(struct iommu_domain 
*domain, struct device *dev)
                arm_smmu_detach_dev(dev);
        }
 
+       mutex_lock(&smmu->domains_mutex);
        mutex_lock(&smmu_domain->init_mutex);
 
        if (!smmu_domain->smmu) {
@@ -2726,6 +2813,7 @@ static int arm_smmu_attach_dev(struct iommu_domain 
*domain, struct device *dev)
 
 out_unlock:
        mutex_unlock(&smmu_domain->init_mutex);
+       mutex_unlock(&smmu->domains_mutex);
 
        iommu_group_put(group);
 
@@ -3330,9 +3418,11 @@ static int arm_smmu_init_structures(struct 
arm_smmu_device *smmu)
 {
        int ret;
 
+       mutex_init(&smmu->domains_mutex);
        spin_lock_init(&smmu->contexts_lock);
        smmu->streams = RB_ROOT;
        INIT_LIST_HEAD(&smmu->tasks);
+       INIT_LIST_HEAD(&smmu->domains);
 
        ret = arm_smmu_init_queues(smmu);
        if (ret)
-- 
2.11.0

_______________________________________________
iommu mailing list
iommu@lists.linux-foundation.org
https://lists.linuxfoundation.org/mailman/listinfo/iommu

Reply via email to