| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (c) 2025, Linaro Limited |
| */ |
| |
| #include <linux/dma-buf.h> |
| #include <linux/dma-heap.h> |
| #include <linux/genalloc.h> |
| #include <linux/module.h> |
| #include <linux/scatterlist.h> |
| #include <linux/slab.h> |
| #include <linux/tee_core.h> |
| #include <linux/xarray.h> |
| |
| #include "tee_private.h" |
| |
| struct tee_dma_heap { |
| struct dma_heap *heap; |
| enum tee_dma_heap_id id; |
| struct kref kref; |
| struct tee_protmem_pool *pool; |
| struct tee_device *teedev; |
| bool shutting_down; |
| /* Protects pool, teedev, and shutting_down above */ |
| struct mutex mu; |
| }; |
| |
| struct tee_heap_buffer { |
| struct tee_dma_heap *heap; |
| size_t size; |
| size_t offs; |
| struct sg_table table; |
| }; |
| |
| struct tee_heap_attachment { |
| struct sg_table table; |
| struct device *dev; |
| }; |
| |
| struct tee_protmem_static_pool { |
| struct tee_protmem_pool pool; |
| struct gen_pool *gen_pool; |
| phys_addr_t pa_base; |
| }; |
| |
| #if IS_ENABLED(CONFIG_TEE_DMABUF_HEAPS) |
| static DEFINE_XARRAY_ALLOC(tee_dma_heap); |
| |
| static void tee_heap_release(struct kref *kref) |
| { |
| struct tee_dma_heap *h = container_of(kref, struct tee_dma_heap, kref); |
| |
| h->pool->ops->destroy_pool(h->pool); |
| tee_device_put(h->teedev); |
| h->pool = NULL; |
| h->teedev = NULL; |
| } |
| |
| static void put_tee_heap(struct tee_dma_heap *h) |
| { |
| kref_put(&h->kref, tee_heap_release); |
| } |
| |
| static void get_tee_heap(struct tee_dma_heap *h) |
| { |
| kref_get(&h->kref); |
| } |
| |
| static int copy_sg_table(struct sg_table *dst, struct sg_table *src) |
| { |
| struct scatterlist *dst_sg; |
| struct scatterlist *src_sg; |
| int ret; |
| int i; |
| |
| ret = sg_alloc_table(dst, src->orig_nents, GFP_KERNEL); |
| if (ret) |
| return ret; |
| |
| dst_sg = dst->sgl; |
| for_each_sgtable_sg(src, src_sg, i) { |
| sg_set_page(dst_sg, sg_page(src_sg), src_sg->length, |
| src_sg->offset); |
| dst_sg = sg_next(dst_sg); |
| } |
| |
| return 0; |
| } |
| |
| static int tee_heap_attach(struct dma_buf *dmabuf, |
| struct dma_buf_attachment *attachment) |
| { |
| struct tee_heap_buffer *buf = dmabuf->priv; |
| struct tee_heap_attachment *a; |
| int ret; |
| |
| a = kzalloc(sizeof(*a), GFP_KERNEL); |
| if (!a) |
| return -ENOMEM; |
| |
| ret = copy_sg_table(&a->table, &buf->table); |
| if (ret) { |
| kfree(a); |
| return ret; |
| } |
| |
| a->dev = attachment->dev; |
| attachment->priv = a; |
| |
| return 0; |
| } |
| |
| static void tee_heap_detach(struct dma_buf *dmabuf, |
| struct dma_buf_attachment *attachment) |
| { |
| struct tee_heap_attachment *a = attachment->priv; |
| |
| sg_free_table(&a->table); |
| kfree(a); |
| } |
| |
| static struct sg_table * |
| tee_heap_map_dma_buf(struct dma_buf_attachment *attachment, |
| enum dma_data_direction direction) |
| { |
| struct tee_heap_attachment *a = attachment->priv; |
| int ret; |
| |
| ret = dma_map_sgtable(attachment->dev, &a->table, direction, |
| DMA_ATTR_SKIP_CPU_SYNC); |
| if (ret) |
| return ERR_PTR(ret); |
| |
| return &a->table; |
| } |
| |
| static void tee_heap_unmap_dma_buf(struct dma_buf_attachment *attachment, |
| struct sg_table *table, |
| enum dma_data_direction direction) |
| { |
| struct tee_heap_attachment *a = attachment->priv; |
| |
| WARN_ON(&a->table != table); |
| |
| dma_unmap_sgtable(attachment->dev, table, direction, |
| DMA_ATTR_SKIP_CPU_SYNC); |
| } |
| |
| static void tee_heap_buf_free(struct dma_buf *dmabuf) |
| { |
| struct tee_heap_buffer *buf = dmabuf->priv; |
| |
| buf->heap->pool->ops->free(buf->heap->pool, &buf->table); |
| mutex_lock(&buf->heap->mu); |
| put_tee_heap(buf->heap); |
| mutex_unlock(&buf->heap->mu); |
| kfree(buf); |
| } |
| |
| static const struct dma_buf_ops tee_heap_buf_ops = { |
| .attach = tee_heap_attach, |
| .detach = tee_heap_detach, |
| .map_dma_buf = tee_heap_map_dma_buf, |
| .unmap_dma_buf = tee_heap_unmap_dma_buf, |
| .release = tee_heap_buf_free, |
| }; |
| |
| static struct dma_buf *tee_dma_heap_alloc(struct dma_heap *heap, |
| unsigned long len, u32 fd_flags, |
| u64 heap_flags) |
| { |
| struct tee_dma_heap *h = dma_heap_get_drvdata(heap); |
| DEFINE_DMA_BUF_EXPORT_INFO(exp_info); |
| struct tee_device *teedev = NULL; |
| struct tee_heap_buffer *buf; |
| struct tee_protmem_pool *pool; |
| struct dma_buf *dmabuf; |
| int rc; |
| |
| mutex_lock(&h->mu); |
| if (h->teedev) { |
| teedev = h->teedev; |
| pool = h->pool; |
| get_tee_heap(h); |
| } |
| mutex_unlock(&h->mu); |
| |
| if (!teedev) |
| return ERR_PTR(-EINVAL); |
| |
| buf = kzalloc(sizeof(*buf), GFP_KERNEL); |
| if (!buf) { |
| dmabuf = ERR_PTR(-ENOMEM); |
| goto err; |
| } |
| buf->size = len; |
| buf->heap = h; |
| |
| rc = pool->ops->alloc(pool, &buf->table, len, &buf->offs); |
| if (rc) { |
| dmabuf = ERR_PTR(rc); |
| goto err_kfree; |
| } |
| |
| exp_info.ops = &tee_heap_buf_ops; |
| exp_info.size = len; |
| exp_info.priv = buf; |
| exp_info.flags = fd_flags; |
| dmabuf = dma_buf_export(&exp_info); |
| if (IS_ERR(dmabuf)) |
| goto err_protmem_free; |
| |
| return dmabuf; |
| |
| err_protmem_free: |
| pool->ops->free(pool, &buf->table); |
| err_kfree: |
| kfree(buf); |
| err: |
| mutex_lock(&h->mu); |
| put_tee_heap(h); |
| mutex_unlock(&h->mu); |
| return dmabuf; |
| } |
| |
| static const struct dma_heap_ops tee_dma_heap_ops = { |
| .allocate = tee_dma_heap_alloc, |
| }; |
| |
| static const char *heap_id_2_name(enum tee_dma_heap_id id) |
| { |
| switch (id) { |
| case TEE_DMA_HEAP_SECURE_VIDEO_PLAY: |
| return "protected,secure-video"; |
| case TEE_DMA_HEAP_TRUSTED_UI: |
| return "protected,trusted-ui"; |
| case TEE_DMA_HEAP_SECURE_VIDEO_RECORD: |
| return "protected,secure-video-record"; |
| default: |
| return NULL; |
| } |
| } |
| |
| static int alloc_dma_heap(struct tee_device *teedev, enum tee_dma_heap_id id, |
| struct tee_protmem_pool *pool) |
| { |
| struct dma_heap_export_info exp_info = { |
| .ops = &tee_dma_heap_ops, |
| .name = heap_id_2_name(id), |
| }; |
| struct tee_dma_heap *h; |
| int rc; |
| |
| if (!exp_info.name) |
| return -EINVAL; |
| |
| if (xa_reserve(&tee_dma_heap, id, GFP_KERNEL)) { |
| if (!xa_load(&tee_dma_heap, id)) |
| return -EEXIST; |
| return -ENOMEM; |
| } |
| |
| h = kzalloc(sizeof(*h), GFP_KERNEL); |
| if (!h) |
| return -ENOMEM; |
| h->id = id; |
| kref_init(&h->kref); |
| h->teedev = teedev; |
| h->pool = pool; |
| mutex_init(&h->mu); |
| |
| exp_info.priv = h; |
| h->heap = dma_heap_add(&exp_info); |
| if (IS_ERR(h->heap)) { |
| rc = PTR_ERR(h->heap); |
| kfree(h); |
| |
| return rc; |
| } |
| |
| /* "can't fail" due to the call to xa_reserve() above */ |
| return WARN_ON(xa_is_err(xa_store(&tee_dma_heap, id, h, GFP_KERNEL))); |
| } |
| |
| int tee_device_register_dma_heap(struct tee_device *teedev, |
| enum tee_dma_heap_id id, |
| struct tee_protmem_pool *pool) |
| { |
| struct tee_dma_heap *h; |
| int rc; |
| |
| if (!tee_device_get(teedev)) |
| return -EINVAL; |
| |
| h = xa_load(&tee_dma_heap, id); |
| if (h) { |
| mutex_lock(&h->mu); |
| if (h->teedev) { |
| rc = -EBUSY; |
| } else { |
| kref_init(&h->kref); |
| h->shutting_down = false; |
| h->teedev = teedev; |
| h->pool = pool; |
| rc = 0; |
| } |
| mutex_unlock(&h->mu); |
| } else { |
| rc = alloc_dma_heap(teedev, id, pool); |
| } |
| |
| if (rc) { |
| tee_device_put(teedev); |
| dev_err(&teedev->dev, "can't register DMA heap id %d (%s)\n", |
| id, heap_id_2_name(id)); |
| } |
| |
| return rc; |
| } |
| EXPORT_SYMBOL_GPL(tee_device_register_dma_heap); |
| |
| void tee_device_put_all_dma_heaps(struct tee_device *teedev) |
| { |
| struct tee_dma_heap *h; |
| u_long i; |
| |
| xa_for_each(&tee_dma_heap, i, h) { |
| if (h) { |
| mutex_lock(&h->mu); |
| if (h->teedev == teedev && !h->shutting_down) { |
| h->shutting_down = true; |
| put_tee_heap(h); |
| } |
| mutex_unlock(&h->mu); |
| } |
| } |
| } |
| EXPORT_SYMBOL_GPL(tee_device_put_all_dma_heaps); |
| |
| int tee_heap_update_from_dma_buf(struct tee_device *teedev, |
| struct dma_buf *dmabuf, size_t *offset, |
| struct tee_shm *shm, |
| struct tee_shm **parent_shm) |
| { |
| struct tee_heap_buffer *buf; |
| int rc; |
| |
| /* The DMA-buf must be from our heap */ |
| if (dmabuf->ops != &tee_heap_buf_ops) |
| return -EINVAL; |
| |
| buf = dmabuf->priv; |
| /* The buffer must be from the same teedev */ |
| if (buf->heap->teedev != teedev) |
| return -EINVAL; |
| |
| shm->size = buf->size; |
| |
| rc = buf->heap->pool->ops->update_shm(buf->heap->pool, &buf->table, |
| buf->offs, shm, parent_shm); |
| if (!rc && *parent_shm) |
| *offset = buf->offs; |
| |
| return rc; |
| } |
| #else |
| int tee_device_register_dma_heap(struct tee_device *teedev __always_unused, |
| enum tee_dma_heap_id id __always_unused, |
| struct tee_protmem_pool *pool __always_unused) |
| { |
| return -EINVAL; |
| } |
| EXPORT_SYMBOL_GPL(tee_device_register_dma_heap); |
| |
| void |
| tee_device_put_all_dma_heaps(struct tee_device *teedev __always_unused) |
| { |
| } |
| EXPORT_SYMBOL_GPL(tee_device_put_all_dma_heaps); |
| |
| int tee_heap_update_from_dma_buf(struct tee_device *teedev __always_unused, |
| struct dma_buf *dmabuf __always_unused, |
| size_t *offset __always_unused, |
| struct tee_shm *shm __always_unused, |
| struct tee_shm **parent_shm __always_unused) |
| { |
| return -EINVAL; |
| } |
| #endif |
| |
| static struct tee_protmem_static_pool * |
| to_protmem_static_pool(struct tee_protmem_pool *pool) |
| { |
| return container_of(pool, struct tee_protmem_static_pool, pool); |
| } |
| |
| static int protmem_pool_op_static_alloc(struct tee_protmem_pool *pool, |
| struct sg_table *sgt, size_t size, |
| size_t *offs) |
| { |
| struct tee_protmem_static_pool *stp = to_protmem_static_pool(pool); |
| phys_addr_t pa; |
| int ret; |
| |
| pa = gen_pool_alloc(stp->gen_pool, size); |
| if (!pa) |
| return -ENOMEM; |
| |
| ret = sg_alloc_table(sgt, 1, GFP_KERNEL); |
| if (ret) { |
| gen_pool_free(stp->gen_pool, pa, size); |
| return ret; |
| } |
| |
| sg_set_page(sgt->sgl, phys_to_page(pa), size, 0); |
| *offs = pa - stp->pa_base; |
| |
| return 0; |
| } |
| |
| static void protmem_pool_op_static_free(struct tee_protmem_pool *pool, |
| struct sg_table *sgt) |
| { |
| struct tee_protmem_static_pool *stp = to_protmem_static_pool(pool); |
| struct scatterlist *sg; |
| int i; |
| |
| for_each_sgtable_sg(sgt, sg, i) |
| gen_pool_free(stp->gen_pool, sg_phys(sg), sg->length); |
| sg_free_table(sgt); |
| } |
| |
| static int protmem_pool_op_static_update_shm(struct tee_protmem_pool *pool, |
| struct sg_table *sgt, size_t offs, |
| struct tee_shm *shm, |
| struct tee_shm **parent_shm) |
| { |
| struct tee_protmem_static_pool *stp = to_protmem_static_pool(pool); |
| |
| shm->paddr = stp->pa_base + offs; |
| *parent_shm = NULL; |
| |
| return 0; |
| } |
| |
| static void protmem_pool_op_static_destroy_pool(struct tee_protmem_pool *pool) |
| { |
| struct tee_protmem_static_pool *stp = to_protmem_static_pool(pool); |
| |
| gen_pool_destroy(stp->gen_pool); |
| kfree(stp); |
| } |
| |
| static struct tee_protmem_pool_ops protmem_pool_ops_static = { |
| .alloc = protmem_pool_op_static_alloc, |
| .free = protmem_pool_op_static_free, |
| .update_shm = protmem_pool_op_static_update_shm, |
| .destroy_pool = protmem_pool_op_static_destroy_pool, |
| }; |
| |
| struct tee_protmem_pool *tee_protmem_static_pool_alloc(phys_addr_t paddr, |
| size_t size) |
| { |
| const size_t page_mask = PAGE_SIZE - 1; |
| struct tee_protmem_static_pool *stp; |
| int rc; |
| |
| /* Check it's page aligned */ |
| if ((paddr | size) & page_mask) |
| return ERR_PTR(-EINVAL); |
| |
| if (!pfn_valid(PHYS_PFN(paddr))) |
| return ERR_PTR(-EINVAL); |
| |
| stp = kzalloc(sizeof(*stp), GFP_KERNEL); |
| if (!stp) |
| return ERR_PTR(-ENOMEM); |
| |
| stp->gen_pool = gen_pool_create(PAGE_SHIFT, -1); |
| if (!stp->gen_pool) { |
| rc = -ENOMEM; |
| goto err_free; |
| } |
| |
| rc = gen_pool_add(stp->gen_pool, paddr, size, -1); |
| if (rc) |
| goto err_free_pool; |
| |
| stp->pool.ops = &protmem_pool_ops_static; |
| stp->pa_base = paddr; |
| return &stp->pool; |
| |
| err_free_pool: |
| gen_pool_destroy(stp->gen_pool); |
| err_free: |
| kfree(stp); |
| |
| return ERR_PTR(rc); |
| } |
| EXPORT_SYMBOL_GPL(tee_protmem_static_pool_alloc); |