]> git.karo-electronics.de Git - karo-tx-linux.git/blobdiff - drivers/iommu/io-pgtable-arm-v7s.c
drm/nouveau/i2c/gf119-: add support for address-only transactions
[karo-tx-linux.git] / drivers / iommu / io-pgtable-arm-v7s.c
index 8d6ca28c3e1f14a6c364aae89640ee6535f8f0d6..af330f513653d2849682b2d4536dd86dcb6a43d7 100644 (file)
@@ -32,6 +32,7 @@
 
 #define pr_fmt(fmt)    "arm-v7s io-pgtable: " fmt
 
+#include <linux/atomic.h>
 #include <linux/dma-mapping.h>
 #include <linux/gfp.h>
 #include <linux/iommu.h>
@@ -39,6 +40,7 @@
 #include <linux/kmemleak.h>
 #include <linux/sizes.h>
 #include <linux/slab.h>
+#include <linux/spinlock.h>
 #include <linux/types.h>
 
 #include <asm/barrier.h>
@@ -92,7 +94,8 @@
 #define ARM_V7S_PTE_TYPE_CONT_PAGE     0x1
 
 #define ARM_V7S_PTE_IS_VALID(pte)      (((pte) & 0x3) != 0)
-#define ARM_V7S_PTE_IS_TABLE(pte, lvl) (lvl == 1 && ((pte) & ARM_V7S_PTE_TYPE_TABLE))
+#define ARM_V7S_PTE_IS_TABLE(pte, lvl) \
+       ((lvl) == 1 && (((pte) & 0x3) == ARM_V7S_PTE_TYPE_TABLE))
 
 /* Page table bits */
 #define ARM_V7S_ATTR_XN(lvl)           BIT(4 * (2 - (lvl)))
@@ -167,6 +170,7 @@ struct arm_v7s_io_pgtable {
 
        arm_v7s_iopte           *pgd;
        struct kmem_cache       *l2_tables;
+       spinlock_t              split_lock;
 };
 
 static dma_addr_t __arm_v7s_dma_addr(void *pages)
@@ -186,7 +190,8 @@ static arm_v7s_iopte *iopte_deref(arm_v7s_iopte pte, int lvl)
 static void *__arm_v7s_alloc_table(int lvl, gfp_t gfp,
                                   struct arm_v7s_io_pgtable *data)
 {
-       struct device *dev = data->iop.cfg.iommu_dev;
+       struct io_pgtable_cfg *cfg = &data->iop.cfg;
+       struct device *dev = cfg->iommu_dev;
        dma_addr_t dma;
        size_t size = ARM_V7S_TABLE_SIZE(lvl);
        void *table = NULL;
@@ -195,7 +200,7 @@ static void *__arm_v7s_alloc_table(int lvl, gfp_t gfp,
                table = (void *)__get_dma_pages(__GFP_ZERO, get_order(size));
        else if (lvl == 2)
                table = kmem_cache_zalloc(data->l2_tables, gfp | GFP_DMA);
-       if (table && !selftest_running) {
+       if (table && !(cfg->quirks & IO_PGTABLE_QUIRK_NO_DMA)) {
                dma = dma_map_single(dev, table, size, DMA_TO_DEVICE);
                if (dma_mapping_error(dev, dma))
                        goto out_free;
@@ -224,10 +229,11 @@ out_free:
 static void __arm_v7s_free_table(void *table, int lvl,
                                 struct arm_v7s_io_pgtable *data)
 {
-       struct device *dev = data->iop.cfg.iommu_dev;
+       struct io_pgtable_cfg *cfg = &data->iop.cfg;
+       struct device *dev = cfg->iommu_dev;
        size_t size = ARM_V7S_TABLE_SIZE(lvl);
 
-       if (!selftest_running)
+       if (!(cfg->quirks & IO_PGTABLE_QUIRK_NO_DMA))
                dma_unmap_single(dev, __arm_v7s_dma_addr(table), size,
                                 DMA_TO_DEVICE);
        if (lvl == 1)
@@ -239,7 +245,7 @@ static void __arm_v7s_free_table(void *table, int lvl,
 static void __arm_v7s_pte_sync(arm_v7s_iopte *ptep, int num_entries,
                               struct io_pgtable_cfg *cfg)
 {
-       if (selftest_running)
+       if (!(cfg->quirks & IO_PGTABLE_QUIRK_NO_DMA))
                return;
 
        dma_sync_single_for_device(cfg->iommu_dev, __arm_v7s_dma_addr(ptep),
@@ -280,6 +286,13 @@ static arm_v7s_iopte arm_v7s_prot_to_pte(int prot, int lvl,
        else if (prot & IOMMU_CACHE)
                pte |= ARM_V7S_ATTR_B | ARM_V7S_ATTR_C;
 
+       pte |= ARM_V7S_PTE_TYPE_PAGE;
+       if (lvl == 1 && (cfg->quirks & IO_PGTABLE_QUIRK_ARM_NS))
+               pte |= ARM_V7S_ATTR_NS_SECTION;
+
+       if (cfg->quirks & IO_PGTABLE_QUIRK_ARM_MTK_4GB)
+               pte |= ARM_V7S_ATTR_MTK_4GB;
+
        return pte;
 }
 
@@ -352,7 +365,7 @@ static int arm_v7s_init_pte(struct arm_v7s_io_pgtable *data,
                            int lvl, int num_entries, arm_v7s_iopte *ptep)
 {
        struct io_pgtable_cfg *cfg = &data->iop.cfg;
-       arm_v7s_iopte pte = arm_v7s_prot_to_pte(prot, lvl, cfg);
+       arm_v7s_iopte pte;
        int i;
 
        for (i = 0; i < num_entries; i++)
@@ -374,13 +387,7 @@ static int arm_v7s_init_pte(struct arm_v7s_io_pgtable *data,
                        return -EEXIST;
                }
 
-       pte |= ARM_V7S_PTE_TYPE_PAGE;
-       if (lvl == 1 && (cfg->quirks & IO_PGTABLE_QUIRK_ARM_NS))
-               pte |= ARM_V7S_ATTR_NS_SECTION;
-
-       if (cfg->quirks & IO_PGTABLE_QUIRK_ARM_MTK_4GB)
-               pte |= ARM_V7S_ATTR_MTK_4GB;
-
+       pte = arm_v7s_prot_to_pte(prot, lvl, cfg);
        if (num_entries > 1)
                pte = arm_v7s_pte_to_cont(pte, lvl);
 
@@ -390,6 +397,30 @@ static int arm_v7s_init_pte(struct arm_v7s_io_pgtable *data,
        return 0;
 }
 
+static arm_v7s_iopte arm_v7s_install_table(arm_v7s_iopte *table,
+                                          arm_v7s_iopte *ptep,
+                                          arm_v7s_iopte curr,
+                                          struct io_pgtable_cfg *cfg)
+{
+       arm_v7s_iopte old, new;
+
+       new = virt_to_phys(table) | ARM_V7S_PTE_TYPE_TABLE;
+       if (cfg->quirks & IO_PGTABLE_QUIRK_ARM_NS)
+               new |= ARM_V7S_ATTR_NS_TABLE;
+
+       /*
+        * Ensure the table itself is visible before its PTE can be.
+        * Whilst we could get away with cmpxchg64_release below, this
+        * doesn't have any ordering semantics when !CONFIG_SMP.
+        */
+       dma_wmb();
+
+       old = cmpxchg_relaxed(ptep, curr, new);
+       __arm_v7s_pte_sync(ptep, 1, cfg);
+
+       return old;
+}
+
 static int __arm_v7s_map(struct arm_v7s_io_pgtable *data, unsigned long iova,
                         phys_addr_t paddr, size_t size, int prot,
                         int lvl, arm_v7s_iopte *ptep)
@@ -411,20 +442,23 @@ static int __arm_v7s_map(struct arm_v7s_io_pgtable *data, unsigned long iova,
                return -EINVAL;
 
        /* Grab a pointer to the next level */
-       pte = *ptep;
+       pte = READ_ONCE(*ptep);
        if (!pte) {
                cptep = __arm_v7s_alloc_table(lvl + 1, GFP_ATOMIC, data);
                if (!cptep)
                        return -ENOMEM;
 
-               pte = virt_to_phys(cptep) | ARM_V7S_PTE_TYPE_TABLE;
-               if (cfg->quirks & IO_PGTABLE_QUIRK_ARM_NS)
-                       pte |= ARM_V7S_ATTR_NS_TABLE;
+               pte = arm_v7s_install_table(cptep, ptep, 0, cfg);
+               if (pte)
+                       __arm_v7s_free_table(cptep, lvl + 1, data);
+       } else {
+               /* We've no easy way of knowing if it's synced yet, so... */
+               __arm_v7s_pte_sync(ptep, 1, cfg);
+       }
 
-               __arm_v7s_set_pte(ptep, pte, 1, cfg);
-       } else if (ARM_V7S_PTE_IS_TABLE(pte, lvl)) {
+       if (ARM_V7S_PTE_IS_TABLE(pte, lvl)) {
                cptep = iopte_deref(pte, lvl);
-       } else {
+       } else if (pte) {
                /* We require an unmap first */
                WARN_ON(!selftest_running);
                return -EEXIST;
@@ -477,66 +511,73 @@ static void arm_v7s_free_pgtable(struct io_pgtable *iop)
        kfree(data);
 }
 
-static void arm_v7s_split_cont(struct arm_v7s_io_pgtable *data,
-                              unsigned long iova, int idx, int lvl,
-                              arm_v7s_iopte *ptep)
+static arm_v7s_iopte arm_v7s_split_cont(struct arm_v7s_io_pgtable *data,
+                                       unsigned long iova, int idx, int lvl,
+                                       arm_v7s_iopte *ptep)
 {
        struct io_pgtable *iop = &data->iop;
        arm_v7s_iopte pte;
        size_t size = ARM_V7S_BLOCK_SIZE(lvl);
        int i;
 
+       /* Check that we didn't lose a race to get the lock */
+       pte = *ptep;
+       if (!arm_v7s_pte_is_cont(pte, lvl))
+               return pte;
+
        ptep -= idx & (ARM_V7S_CONT_PAGES - 1);
-       pte = arm_v7s_cont_to_pte(*ptep, lvl);
-       for (i = 0; i < ARM_V7S_CONT_PAGES; i++) {
-               ptep[i] = pte;
-               pte += size;
-       }
+       pte = arm_v7s_cont_to_pte(pte, lvl);
+       for (i = 0; i < ARM_V7S_CONT_PAGES; i++)
+               ptep[i] = pte + i * size;
 
        __arm_v7s_pte_sync(ptep, ARM_V7S_CONT_PAGES, &iop->cfg);
 
        size *= ARM_V7S_CONT_PAGES;
        io_pgtable_tlb_add_flush(iop, iova, size, size, true);
        io_pgtable_tlb_sync(iop);
+       return pte;
 }
 
 static int arm_v7s_split_blk_unmap(struct arm_v7s_io_pgtable *data,
                                   unsigned long iova, size_t size,
-                                  arm_v7s_iopte *ptep)
+                                  arm_v7s_iopte blk_pte, arm_v7s_iopte *ptep)
 {
-       unsigned long blk_start, blk_end, blk_size;
-       phys_addr_t blk_paddr;
-       arm_v7s_iopte table = 0;
-       int prot = arm_v7s_pte_to_prot(*ptep, 1);
+       struct io_pgtable_cfg *cfg = &data->iop.cfg;
+       arm_v7s_iopte pte, *tablep;
+       int i, unmap_idx, num_entries, num_ptes;
 
-       blk_size = ARM_V7S_BLOCK_SIZE(1);
-       blk_start = iova & ARM_V7S_LVL_MASK(1);
-       blk_end = blk_start + ARM_V7S_BLOCK_SIZE(1);
-       blk_paddr = *ptep & ARM_V7S_LVL_MASK(1);
+       tablep = __arm_v7s_alloc_table(2, GFP_ATOMIC, data);
+       if (!tablep)
+               return 0; /* Bytes unmapped */
 
-       for (; blk_start < blk_end; blk_start += size, blk_paddr += size) {
-               arm_v7s_iopte *tablep;
+       num_ptes = ARM_V7S_PTES_PER_LVL(2);
+       num_entries = size >> ARM_V7S_LVL_SHIFT(2);
+       unmap_idx = ARM_V7S_LVL_IDX(iova, 2);
 
+       pte = arm_v7s_prot_to_pte(arm_v7s_pte_to_prot(blk_pte, 1), 2, cfg);
+       if (num_entries > 1)
+               pte = arm_v7s_pte_to_cont(pte, 2);
+
+       for (i = 0; i < num_ptes; i += num_entries, pte += size) {
                /* Unmap! */
-               if (blk_start == iova)
+               if (i == unmap_idx)
                        continue;
 
-               /* __arm_v7s_map expects a pointer to the start of the table */
-               tablep = &table - ARM_V7S_LVL_IDX(blk_start, 1);
-               if (__arm_v7s_map(data, blk_start, blk_paddr, size, prot, 1,
-                                 tablep) < 0) {
-                       if (table) {
-                               /* Free the table we allocated */
-                               tablep = iopte_deref(table, 1);
-                               __arm_v7s_free_table(tablep, 2, data);
-                       }
-                       return 0; /* Bytes unmapped */
-               }
+               __arm_v7s_set_pte(&tablep[i], pte, num_entries, cfg);
        }
 
-       __arm_v7s_set_pte(ptep, table, 1, &data->iop.cfg);
-       iova &= ~(blk_size - 1);
-       io_pgtable_tlb_add_flush(&data->iop, iova, blk_size, blk_size, true);
+       pte = arm_v7s_install_table(tablep, ptep, blk_pte, cfg);
+       if (pte != blk_pte) {
+               __arm_v7s_free_table(tablep, 2, data);
+
+               if (!ARM_V7S_PTE_IS_TABLE(pte, 1))
+                       return 0;
+
+               tablep = iopte_deref(pte, 1);
+               return __arm_v7s_unmap(data, iova, size, 2, tablep);
+       }
+
+       io_pgtable_tlb_add_flush(&data->iop, iova, size, size, true);
        return size;
 }
 
@@ -555,17 +596,28 @@ static int __arm_v7s_unmap(struct arm_v7s_io_pgtable *data,
        idx = ARM_V7S_LVL_IDX(iova, lvl);
        ptep += idx;
        do {
-               if (WARN_ON(!ARM_V7S_PTE_IS_VALID(ptep[i])))
+               pte[i] = READ_ONCE(ptep[i]);
+               if (WARN_ON(!ARM_V7S_PTE_IS_VALID(pte[i])))
                        return 0;
-               pte[i] = ptep[i];
        } while (++i < num_entries);
 
        /*
         * If we've hit a contiguous 'large page' entry at this level, it
         * needs splitting first, unless we're unmapping the whole lot.
+        *
+        * For splitting, we can't rewrite 16 PTEs atomically, and since we
+        * can't necessarily assume TEX remap we don't have a software bit to
+        * mark live entries being split. In practice (i.e. DMA API code), we
+        * will never be splitting large pages anyway, so just wrap this edge
+        * case in a lock for the sake of correctness and be done with it.
         */
-       if (num_entries <= 1 && arm_v7s_pte_is_cont(pte[0], lvl))
-               arm_v7s_split_cont(data, iova, idx, lvl, ptep);
+       if (num_entries <= 1 && arm_v7s_pte_is_cont(pte[0], lvl)) {
+               unsigned long flags;
+
+               spin_lock_irqsave(&data->split_lock, flags);
+               pte[0] = arm_v7s_split_cont(data, iova, idx, lvl, ptep);
+               spin_unlock_irqrestore(&data->split_lock, flags);
+       }
 
        /* If the size matches this level, we're in the right place */
        if (num_entries) {
@@ -593,7 +645,7 @@ static int __arm_v7s_unmap(struct arm_v7s_io_pgtable *data,
                 * Insert a table at the next level to map the old region,
                 * minus the part we want to unmap
                 */
-               return arm_v7s_split_blk_unmap(data, iova, size, ptep);
+               return arm_v7s_split_blk_unmap(data, iova, size, pte[0], ptep);
        }
 
        /* Keep on walkin' */
@@ -623,7 +675,8 @@ static phys_addr_t arm_v7s_iova_to_phys(struct io_pgtable_ops *ops,
        u32 mask;
 
        do {
-               pte = ptep[ARM_V7S_LVL_IDX(iova, ++lvl)];
+               ptep += ARM_V7S_LVL_IDX(iova, ++lvl);
+               pte = READ_ONCE(*ptep);
                ptep = iopte_deref(pte, lvl);
        } while (ARM_V7S_PTE_IS_TABLE(pte, lvl));
 
@@ -651,7 +704,8 @@ static struct io_pgtable *arm_v7s_alloc_pgtable(struct io_pgtable_cfg *cfg,
        if (cfg->quirks & ~(IO_PGTABLE_QUIRK_ARM_NS |
                            IO_PGTABLE_QUIRK_NO_PERMS |
                            IO_PGTABLE_QUIRK_TLBI_ON_MAP |
-                           IO_PGTABLE_QUIRK_ARM_MTK_4GB))
+                           IO_PGTABLE_QUIRK_ARM_MTK_4GB |
+                           IO_PGTABLE_QUIRK_NO_DMA))
                return NULL;
 
        /* If ARM_MTK_4GB is enabled, the NO_PERMS is also expected. */
@@ -663,6 +717,7 @@ static struct io_pgtable *arm_v7s_alloc_pgtable(struct io_pgtable_cfg *cfg,
        if (!data)
                return NULL;
 
+       spin_lock_init(&data->split_lock);
        data->l2_tables = kmem_cache_create("io-pgtable_armv7s_l2",
                                            ARM_V7S_TABLE_SIZE(2),
                                            ARM_V7S_TABLE_SIZE(2),
@@ -749,7 +804,7 @@ static void dummy_tlb_sync(void *cookie)
        WARN_ON(cookie != cfg_cookie);
 }
 
-static struct iommu_gather_ops dummy_tlb_ops = {
+static const struct iommu_gather_ops dummy_tlb_ops = {
        .tlb_flush_all  = dummy_tlb_flush_all,
        .tlb_add_flush  = dummy_tlb_add_flush,
        .tlb_sync       = dummy_tlb_sync,
@@ -768,7 +823,7 @@ static int __init arm_v7s_do_selftests(void)
                .tlb = &dummy_tlb_ops,
                .oas = 32,
                .ias = 32,
-               .quirks = IO_PGTABLE_QUIRK_ARM_NS,
+               .quirks = IO_PGTABLE_QUIRK_ARM_NS | IO_PGTABLE_QUIRK_NO_DMA,
                .pgsize_bitmap = SZ_4K | SZ_64K | SZ_1M | SZ_16M,
        };
        unsigned int iova, size, iova_start;