This is my attempt to shrink 'dma_free_o' and 'dma_in_use' in 'struct
page' (originally 'offset' and 'in_use' in 'struct dma_page') to 16-bit
so that it is unnecessary to use the '_mapcount' field of 'struct
page'.  However, it adds complexity and makes allocating and freeing up
to 20% slower for little gain, so I am NOT recommending that it be
merged at this time.  I am posting it just for reference in case someone
finds it useful in the future.

The main difficulty is supporting archs that have PAGE_SIZE > 64 KiB,
for which a 16-bit byte offset is insufficient to cover the entire
page.  So I took the approach of converting everything from a "byte
offset" into a "block index".  That way the code can split any PAGE_SIZE
into as many as 65535 blocks (one 16-bit index value is reserved for the
list terminator).  For example, with PAGE_SIZE of 1 MiB, you get 65535
blocks for 'size' <= 16.  But that introduces a lot of ugly math due to
the 'boundary' checking, which makes the code slower and more complex.

I wrote a standalone program that iterates over all the combinations of
PAGE_SIZE, 'size', and 'boundary', and performs a series of consistency
checks on pool_blk_idx_to_offset(), pool_offset_to_blk_idx(), and
pool_initialize_free_block_list().  The math may be ugly but I am pretty
sure it is correct.

One of the nice things about this is that dma_pool_free() can do some
additional sanity checks:
*) Check that the offset of the passed-in address corresponds to a valid
block offset.
*) With DMAPOOL_DEBUG enabled, check that the number of blocks in the
freelist exactly matches the number that should be there.  This improves
the debug check I added in a previous patch by adding the calculation
for pool->blks_per_alloc.

NOT for merging.
---
--- linux/include/linux/mm_types.h.orig 2018-08-01 12:25:25.000000000 -0400
+++ linux/include/linux/mm_types.h      2018-08-01 12:25:52.000000000 -0400
@@ -156,7 +156,8 @@ struct page {
                struct {        /* dma_pool pages */
                        struct list_head dma_list;
                        dma_addr_t dma;
-                       unsigned int dma_free_o;
+                       unsigned short dma_free_idx;
+                       unsigned short dma_in_use;
                };
 
                /** @rcu_head: You can use this to free a page by RCU. */
@@ -180,8 +181,6 @@ struct page {
 
                unsigned int active;            /* SLAB */
                int units;                      /* SLOB */
-
-               unsigned int dma_in_use;        /* dma_pool pages */
        };
 
        /* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
--- linux/mm/dmapool.c.orig     2018-08-02 14:02:42.000000000 -0400
+++ linux/mm/dmapool.c  2018-08-02 14:03:31.000000000 -0400
@@ -51,16 +51,25 @@
 #define DMAPOOL_DEBUG 1
 #endif
 
+/*
+ * This matches the type of struct page::dma_free_idx, which is 16-bit to
+ * conserve space in struct page.
+ */
+typedef unsigned short pool_idx_t;
+#define POOL_IDX_MAX USHRT_MAX
+
 struct dma_pool {              /* the pool */
 #define POOL_FULL_IDX   0
 #define POOL_AVAIL_IDX  1
 #define POOL_N_LISTS    2
        struct list_head page_list[POOL_N_LISTS];
        spinlock_t lock;
-       size_t size;
        struct device *dev;
-       size_t allocation;
-       size_t boundary;
+       unsigned int size;
+       unsigned int allocation;
+       unsigned int boundary_shift;
+       unsigned int blks_per_boundary;
+       unsigned int blks_per_alloc;
        char name[32];
        struct list_head pools;
 };
@@ -103,9 +112,9 @@ show_pools(struct device *dev, struct de
                spin_unlock_irq(&pool->lock);
 
                /* per-pool info, no real statistics yet */
-               temp = scnprintf(next, size, "%-16s %4u %4zu %4zu %2u\n",
+               temp = scnprintf(next, size, "%-16s %4u %4u %4u %2u\n",
                                 pool->name, blocks,
-                                pages * (pool->allocation / pool->size),
+                                pages * pool->blks_per_alloc,
                                 pool->size, pages);
                size -= temp;
                next += temp;
@@ -141,6 +150,7 @@ static DEVICE_ATTR(pools, 0444, show_pool
 struct dma_pool *dma_pool_create(const char *name, struct device *dev,
                                 size_t size, size_t align, size_t boundary)
 {
+       unsigned int boundary_shift;
        struct dma_pool *retval;
        size_t allocation;
        bool empty = false;
@@ -150,10 +160,10 @@ struct dma_pool *dma_pool_create(const c
        else if (align & (align - 1))
                return NULL;
 
-       if (size == 0)
+       if (size == 0 || size > SZ_2G)
                return NULL;
-       else if (size < 4)
-               size = 4;
+       else if (size < sizeof(pool_idx_t))
+               size = sizeof(pool_idx_t);
 
        if ((size % align) != 0)
                size = ALIGN(size, align);
@@ -165,6 +175,9 @@ struct dma_pool *dma_pool_create(const c
        else if ((boundary < size) || (boundary & (boundary - 1)))
                return NULL;
 
+       boundary_shift = get_count_order_long(min(boundary, allocation));
+       boundary = 1U << boundary_shift;
+
        retval = kmalloc_node(sizeof(*retval), GFP_KERNEL, dev_to_node(dev));
        if (!retval)
                return retval;
@@ -177,8 +190,29 @@ struct dma_pool *dma_pool_create(const c
        INIT_LIST_HEAD(&retval->page_list[1]);
        spin_lock_init(&retval->lock);
        retval->size = size;
-       retval->boundary = boundary;
        retval->allocation = allocation;
+       retval->boundary_shift = boundary_shift;
+       retval->blks_per_boundary = boundary / size;
+       retval->blks_per_alloc =
+               (allocation / boundary) * retval->blks_per_boundary +
+               (allocation % boundary) / size;
+       if (boundary >= allocation || boundary % size == 0) {
+               /*
+                * When the blocks are packed together, an individual block
+                * will never cross the boundary, so the boundary doesn't
+                * matter in this case.  Enable some faster codepaths that skip
+                * boundary calculations for a small speedup.
+                */
+               retval->blks_per_boundary = 0;
+       }
+       if (retval->blks_per_alloc > POOL_IDX_MAX) {
+               /*
+                * This would only affect archs with large PAGE_SIZE.  Limit
+                * the total number of blocks per allocation to avoid
+                * overflowing dma_in_use and dma_free_idx.
+                */
+               retval->blks_per_alloc = POOL_IDX_MAX;
+       }
 
        INIT_LIST_HEAD(&retval->pools);
 
@@ -214,20 +248,73 @@ struct dma_pool *dma_pool_create(const c
 }
 EXPORT_SYMBOL(dma_pool_create);
 
+/*
+ * Convert the index of a block of size pool->size to its offset within an
+ * allocated chunk of memory of size pool->allocation.
+ */
+static unsigned int pool_blk_idx_to_offset(struct dma_pool *pool,
+                                          unsigned int blk_idx)
+{
+       unsigned int offset;
+
+       if (pool->blks_per_boundary == 0) {
+               offset = blk_idx * pool->size;
+       } else {
+               offset = ((blk_idx / pool->blks_per_boundary) <<
+                         pool->boundary_shift) +
+                        (blk_idx % pool->blks_per_boundary) * pool->size;
+       }
+       return offset;
+}
+
+/*
+ * Convert an offset within an allocated chunk of memory of size
+ * pool->allocation to the index of the possibly-smaller block of size
+ * pool->size.  If the given offset is not located at the beginning of a valid
+ * block, then the return value will be >= pool->blks_per_alloc.
+ */
+static unsigned int pool_offset_to_blk_idx(struct dma_pool *pool,
+                                          unsigned int offset)
+{
+       unsigned int blk_idx;
+
+       if (pool->blks_per_boundary == 0) {
+               blk_idx = (likely(offset % pool->size == 0))
+                         ? (offset / pool->size)
+                         : pool->blks_per_alloc;
+       } else {
+               unsigned int offset_within_boundary =
+                       offset & ((1U << pool->boundary_shift) - 1);
+               unsigned int idx_within_boundary =
+                       offset_within_boundary / pool->size;
+
+               if (likely(offset_within_boundary % pool->size == 0 &&
+                          idx_within_boundary < pool->blks_per_boundary)) {
+                       blk_idx = (offset >> pool->boundary_shift) *
+                                 pool->blks_per_boundary +
+                                 idx_within_boundary;
+               } else {
+                       blk_idx = pool->blks_per_alloc;
+               }
+       }
+       return blk_idx;
+}
+
 static void pool_initialize_free_block_list(struct dma_pool *pool, void *vaddr)
 {
+       unsigned int next_boundary = 1U << pool->boundary_shift;
        unsigned int offset = 0;
-       unsigned int next_boundary = pool->boundary;
+       unsigned int i;
+
+       for (i = 0; i < pool->blks_per_alloc; i++) {
+               *(pool_idx_t *)(vaddr + offset) = (pool_idx_t) i + 1;
 
-       do {
-               unsigned int next = offset + pool->size;
-               if (unlikely((next + pool->size) > next_boundary)) {
-                       next = next_boundary;
-                       next_boundary += pool->boundary;
+               offset += pool->size;
+               if (unlikely((offset + pool->size) > next_boundary)) {
+                       offset = next_boundary;
+                       next_boundary += 1U << pool->boundary_shift;
                }
-               *(int *)(vaddr + offset) = next;
-               offset = next;
-       } while (offset < pool->allocation);
+       }
 }
 
 static struct page *pool_alloc_page(struct dma_pool *pool, gfp_t mem_flags)
@@ -248,7 +335,7 @@ static struct page *pool_alloc_page(stru
 
        page = virt_to_page(vaddr);
        page->dma = dma;
-       page->dma_free_o = 0;
+       page->dma_free_idx = 0;
        page->dma_in_use = 0;
 
        return page;
@@ -272,8 +359,8 @@ static void pool_free_page(struct dma_po
        page->dma_list.next = NULL;
        page->dma_list.prev = NULL;
        page->dma = 0;
-       page->dma_free_o = 0;
-       page_mapcount_reset(page); /* clear dma_in_use */
+       page->dma_free_idx = 0;
+       page->dma_in_use = 0;
 
        if (busy) {
                dev_err(pool->dev,
@@ -342,9 +429,10 @@ EXPORT_SYMBOL(dma_pool_destroy);
 void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags,
                     dma_addr_t *handle)
 {
+       unsigned int blk_idx;
+       unsigned int offset;
        unsigned long flags;
        struct page *page;
-       size_t offset;
        void *retval;
        void *vaddr;
 
@@ -370,9 +458,10 @@ void *dma_pool_alloc(struct dma_pool *po
  ready:
        vaddr = page_to_virt(page);
        page->dma_in_use++;
-       offset = page->dma_free_o;
-       page->dma_free_o = *(int *)(vaddr + offset);
-       if (page->dma_free_o >= pool->allocation) {
+       blk_idx = page->dma_free_idx;
+       offset = pool_blk_idx_to_offset(pool, blk_idx);
+       page->dma_free_idx = *(pool_idx_t *)(vaddr + offset);
+       if (page->dma_free_idx >= pool->blks_per_alloc) {
                /* Move page from the "available" list to the "full" list. */
                list_del(&page->dma_list);
                list_add(&page->dma_list, &pool->page_list[POOL_FULL_IDX]);
@@ -383,8 +472,8 @@ void *dma_pool_alloc(struct dma_pool *po
        {
                int i;
                u8 *data = retval;
-               /* page->dma_free_o is stored in first 4 bytes */
-               for (i = sizeof(page->dma_free_o); i < pool->size; i++) {
+               /* a pool_idx_t is stored at the beginning of the block */
+               for (i = sizeof(pool_idx_t); i < pool->size; i++) {
                        if (data[i] == POOL_POISON_FREED)
                                continue;
                        dev_err(pool->dev,
@@ -426,6 +515,7 @@ void dma_pool_free(struct dma_pool *pool
        struct page *page;
        unsigned long flags;
        unsigned int offset;
+       unsigned int blk_idx;
 
        if (unlikely(!virt_addr_valid(vaddr))) {
                dev_err(pool->dev,
@@ -438,21 +528,28 @@ void dma_pool_free(struct dma_pool *pool
        offset = offset_in_page(vaddr);
 
        if (unlikely((dma - page->dma) != offset)) {
+ bad_vaddr:
                dev_err(pool->dev,
                        "dma_pool_free %s, %p (bad vaddr)/%pad (or bad dma)\n",
                        pool->name, vaddr, &dma);
                return;
        }
 
+       blk_idx = pool_offset_to_blk_idx(pool, offset);
+       if (unlikely(blk_idx >= pool->blks_per_alloc))
+               goto bad_vaddr;
+
        spin_lock_irqsave(&pool->lock, flags);
 #ifdef DMAPOOL_DEBUG
        {
                void *page_vaddr = vaddr - offset;
-               unsigned int chain = page->dma_free_o;
-               size_t total_free = 0;
+               unsigned int chain_idx = page->dma_free_idx;
+               unsigned int n_free = 0;
+
+               while (chain_idx < pool->blks_per_alloc) {
+                       unsigned int chain_offset;
 
-               while (chain < pool->allocation) {
-                       if (unlikely(chain == offset)) {
+                       if (unlikely(chain_idx == blk_idx)) {
                                spin_unlock_irqrestore(&pool->lock, flags);
                                dev_err(pool->dev,
                                        "dma_pool_free %s, dma %pad already 
free\n",
@@ -461,15 +558,15 @@ void dma_pool_free(struct dma_pool *pool
                        }
 
                        /*
-                        * The calculation of the number of blocks per
-                        * allocation is actually more complicated than this
-                        * because of the boundary value.  But this comparison
-                        * does not need to be exact; it just needs to prevent
-                        * an endless loop in case a buggy driver causes a
-                        * circular loop in the freelist.
+                        * A buggy driver could corrupt the freelist by
+                        * use-after-free, buffer overflow, etc.  Besides
+                        * checking for corruption, this also prevents an
+                        * endless loop in case corruption causes a circular
+                        * loop in the freelist.
                         */
-                       total_free += pool->size;
-                       if (unlikely(total_free >= pool->allocation)) {
+                       if (unlikely(++n_free + page->dma_in_use >
+                                    pool->blks_per_alloc)) {
+ freelist_corrupt:
                                spin_unlock_irqrestore(&pool->lock, flags);
                                dev_err(pool->dev,
                                        "dma_pool_free %s, freelist 
corrupted\n",
@@ -477,20 +574,24 @@ void dma_pool_free(struct dma_pool *pool
                                return;
                        }
 
-                       chain = *(int *)(page_vaddr + chain);
+                       chain_offset = pool_blk_idx_to_offset(pool, chain_idx);
+                       chain_idx =
+                               *(pool_idx_t *) (page_vaddr + chain_offset);
                }
+               if (n_free + page->dma_in_use != pool->blks_per_alloc)
+                       goto freelist_corrupt;
        }
        memset(vaddr, POOL_POISON_FREED, pool->size);
 #endif
 
        page->dma_in_use--;
-       if (page->dma_free_o >= pool->allocation) {
+       if (page->dma_free_idx >= pool->blks_per_alloc) {
                /* Move page from the "full" list to the "available" list. */
                list_del(&page->dma_list);
                list_add(&page->dma_list, &pool->page_list[POOL_AVAIL_IDX]);
        }
-       *(int *)vaddr = page->dma_free_o;
-       page->dma_free_o = offset;
+       *(pool_idx_t *)vaddr = page->dma_free_idx;
+       page->dma_free_idx = blk_idx;
        /*
         * Resist a temptation to do
         *    if (!is_page_busy(page)) pool_free_page(pool, page);

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

Reply via email to