| // 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); |