| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Interface of reserving an IOVA region and map buffer/dma-buf chunks to there. |
| * |
| * Expected functionality of this interface is: |
| * 1. Reserve an IOVA region from the IOMMU domain. |
| * 2. The user will request mapping a buffer/dma-buf to the reserved region with a specific IOVA |
| * address on demand. |
| * |
| * Copyright (C) 2023 Google LLC |
| */ |
| |
| #include <linux/dma-buf.h> |
| #include <linux/kref.h> |
| #include <linux/list.h> |
| #include <linux/mutex.h> |
| #include <linux/rbtree.h> |
| #include <linux/slab.h> |
| |
| #include <gcip/gcip-iommu-reserve.h> |
| #include <gcip/gcip-iommu.h> |
| |
| /* Wrapping mapping structure to be managed by the `struct gcip_iommu_reserve_region`. */ |
| struct gcip_iommu_reserve_mapping { |
| struct gcip_iommu_reserve_region *region; |
| struct gcip_iommu_mapping *mapping; |
| struct list_head node; |
| struct kref kref; |
| void *data; |
| }; |
| |
| /* Releases @mgr. */ |
| static void gcip_iommu_reserve_manager_release(struct kref *kref) |
| { |
| struct gcip_iommu_reserve_manager *mgr = |
| container_of(kref, struct gcip_iommu_reserve_manager, kref); |
| |
| kfree(mgr); |
| } |
| |
| /* Increments the refcount of @mgr. */ |
| static void gcip_iommu_reserve_manager_get(struct gcip_iommu_reserve_manager *mgr) |
| { |
| kref_get(&mgr->kref); |
| } |
| |
| /* Decrements the refcount of @mgr. If it becomes 0, @mgr will be released. */ |
| static void gcip_iommu_reserve_manager_put(struct gcip_iommu_reserve_manager *mgr) |
| { |
| kref_put(&mgr->kref, gcip_iommu_reserve_manager_release); |
| } |
| |
| /* Checks whether @ops has valid operators and set it to @mgr. */ |
| static int gcip_iommu_reserve_manager_set_ops(struct gcip_iommu_reserve_manager *mgr, |
| const struct gcip_iommu_reserve_manager_ops *ops) |
| { |
| if (!ops || !ops->unmap) |
| return -EINVAL; |
| |
| mgr->ops = ops; |
| |
| return 0; |
| } |
| |
| struct gcip_iommu_reserve_manager * |
| gcip_iommu_reserve_manager_create(struct gcip_iommu_domain *domain, |
| const struct gcip_iommu_reserve_manager_ops *ops, void *data) |
| { |
| struct gcip_iommu_reserve_manager *mgr = kzalloc(sizeof(*mgr), GFP_KERNEL); |
| int ret; |
| |
| if (!mgr) |
| return ERR_PTR(-ENOMEM); |
| |
| ret = gcip_iommu_reserve_manager_set_ops(mgr, ops); |
| if (ret) { |
| kfree(mgr); |
| return ERR_PTR(ret); |
| } |
| |
| mgr->domain = domain; |
| mgr->data = data; |
| mgr->regions = RB_ROOT; |
| kref_init(&mgr->kref); |
| mutex_init(&mgr->lock); |
| |
| return mgr; |
| } |
| |
| /* Returns the reserved region to the IOMMU domain and releases it. */ |
| static void gcip_iommu_reserve_region_release(struct kref *kref) |
| { |
| struct gcip_iommu_reserve_region *region = |
| container_of(kref, struct gcip_iommu_reserve_region, kref); |
| |
| /* |
| * Releases a refcount of @mgr which was held by the `gcip_iommu_reserve_region_create` |
| * function. |
| */ |
| gcip_iommu_reserve_manager_put(region->mgr); |
| gcip_iommu_free_iova(region->domain, region->base_daddr, region->size); |
| kfree(region); |
| } |
| |
| /* Increments the refcount of @region. */ |
| static void gcip_iommu_reserve_region_get(struct gcip_iommu_reserve_region *region) |
| { |
| if (!region) |
| return; |
| kref_get(®ion->kref); |
| } |
| |
| /* Decrements the refcount of @region. */ |
| static void gcip_iommu_reserve_region_put(struct gcip_iommu_reserve_region *region) |
| { |
| if (!region) |
| return; |
| kref_put(®ion->kref, gcip_iommu_reserve_region_release); |
| } |
| |
| /* |
| * Releases the `struct gcip_iommu_reserve_mapping` instance which was allocated by the |
| * `gcip_iommu_reserve_mapping_alloc_locked` function. |
| */ |
| static void gcip_iommu_reserve_mapping_release(struct kref *kref) |
| { |
| struct gcip_iommu_reserve_mapping *reserve_mapping = |
| container_of(kref, struct gcip_iommu_reserve_mapping, kref); |
| |
| /* Releases the refcount which was held by `gcip_iommu_reserve_mapping_alloc_locked`. */ |
| gcip_iommu_reserve_region_put(reserve_mapping->region); |
| kfree(reserve_mapping); |
| } |
| |
| /* Increments the refcount of @reserve_mapping. */ |
| static void gcip_iommu_reserve_mapping_get(struct gcip_iommu_reserve_mapping *reserve_mapping) |
| { |
| kref_get(&reserve_mapping->kref); |
| } |
| |
| /* Decrements the refcount of @reserve_mapping. */ |
| static void gcip_iommu_reserve_mapping_put(struct gcip_iommu_reserve_mapping *reserve_mapping) |
| { |
| kref_put(&reserve_mapping->kref, gcip_iommu_reserve_mapping_release); |
| } |
| |
| /* The callback which will be called when @reserve_mapping->mapping is unmapped. */ |
| static void gcip_iommu_reserve_mapping_after_unmap(void *data) |
| { |
| struct gcip_iommu_reserve_mapping *reserve_mapping = data; |
| struct gcip_iommu_reserve_region *region = reserve_mapping->region; |
| |
| mutex_lock(®ion->lock); |
| if (!region->retired) |
| list_del_init(&reserve_mapping->node); |
| mutex_unlock(®ion->lock); |
| |
| gcip_iommu_reserve_mapping_put(reserve_mapping); |
| } |
| |
| static const struct gcip_iommu_mapping_ops reserve_mapping_ops = { |
| .after_unmap = gcip_iommu_reserve_mapping_after_unmap, |
| }; |
| |
| /* |
| * Allocates a `struct gcip_iommu_reserve_mapping` instance which wraps @mapping containing mapping |
| * information of mapped buffer/dma-buf to @region. |
| * |
| * The caller must hold @region->lock. |
| */ |
| static struct gcip_iommu_reserve_mapping * |
| gcip_iommu_reserve_mapping_alloc_locked(struct gcip_iommu_reserve_region *region, |
| struct gcip_iommu_mapping *mapping, void *data) |
| { |
| struct gcip_iommu_reserve_mapping *reserve_mapping; |
| |
| lockdep_assert_held(®ion->lock); |
| |
| reserve_mapping = kzalloc(sizeof(*reserve_mapping), GFP_KERNEL); |
| if (!reserve_mapping) |
| return ERR_PTR(-ENOMEM); |
| |
| /* The reserved region must not be returned until @mapping is unmapped. */ |
| gcip_iommu_reserve_region_get(region); |
| |
| reserve_mapping->region = region; |
| reserve_mapping->mapping = mapping; |
| reserve_mapping->data = data; |
| kref_init(&reserve_mapping->kref); |
| |
| gcip_iommu_mapping_set_ops(mapping, &reserve_mapping_ops); |
| gcip_iommu_mapping_set_data(mapping, reserve_mapping); |
| |
| return reserve_mapping; |
| } |
| |
| /* |
| * Adds @region to the @mgr->regions red-black tree and increments the refcount of @region. |
| * |
| * The caller must hold @mgr->lock. |
| */ |
| static int gcip_iommu_reserve_manager_add_region_locked(struct gcip_iommu_reserve_manager *mgr, |
| struct gcip_iommu_reserve_region *region) |
| { |
| struct rb_node **link = &mgr->regions.rb_node, *parent_node = NULL; |
| struct gcip_iommu_reserve_region *parent; |
| |
| lockdep_assert_held(&mgr->lock); |
| |
| while (*link) { |
| parent_node = *link; |
| parent = rb_entry(parent_node, struct gcip_iommu_reserve_region, node); |
| |
| if (parent->base_daddr > region->base_daddr) |
| link = &(*link)->rb_left; |
| else if (parent->base_daddr < region->base_daddr) |
| link = &(*link)->rb_right; |
| else |
| return -EEXIST; |
| } |
| |
| gcip_iommu_reserve_region_get(region); |
| rb_link_node(®ion->node, parent_node, link); |
| rb_insert_color(®ion->node, &mgr->regions); |
| |
| return 0; |
| } |
| |
| /* |
| * Removes @region from the @mgr->regions red-black tree and decrements the refcount of @region. |
| * |
| * It is not mandatory to hold @mgr->lock if it is guaranteed that @mgr->regions won't be |
| * manipulated in the middle of this function. |
| */ |
| static void gcip_iommu_reserve_manager_remove_region(struct gcip_iommu_reserve_manager *mgr, |
| struct gcip_iommu_reserve_region *region) |
| { |
| rb_erase(®ion->node, ®ion->mgr->regions); |
| gcip_iommu_reserve_region_put(region); |
| } |
| |
| /* |
| * Finds the reserved region starting at @base_daddr from @mgr and increments its refocunt. |
| * |
| * The caller must call the `gcip_iommu_reserve_region_put` function once it doesn't need to access |
| * the returned region anymore. |
| * |
| * Returns the pointer on success. Otherwise, returns a negative errno pointer. |
| * |
| * The caller must hold @mgr->lock. |
| */ |
| static struct gcip_iommu_reserve_region * |
| gcip_iommu_reserve_manager_get_region_locked(struct gcip_iommu_reserve_manager *mgr, |
| dma_addr_t base_daddr) |
| { |
| struct gcip_iommu_reserve_region *region; |
| struct rb_node *node; |
| |
| lockdep_assert_held(&mgr->lock); |
| |
| node = mgr->regions.rb_node; |
| |
| while (node) { |
| region = rb_entry(node, struct gcip_iommu_reserve_region, node); |
| |
| if (region->base_daddr == base_daddr) { |
| gcip_iommu_reserve_region_get(region); |
| return region; |
| } else if (region->base_daddr > base_daddr) { |
| node = node->rb_left; |
| } else { |
| node = node->rb_right; |
| } |
| } |
| |
| return ERR_PTR(-EINVAL); |
| } |
| |
| /* |
| * Finds the reserved region which can map the @size of buffer/dma-buf to the @start IOVA address |
| * from @mgr and increments its refcount. |
| * |
| * The caller must call the `gcip_iommu_reserve_region_put` function below once it doesn't need |
| * to access the returned region anymore. |
| * |
| * Returns the pointer on success. Otherwise, returns a negative errno pointer. |
| * |
| * The caller must hold @mgr->lock. |
| */ |
| static struct gcip_iommu_reserve_region * |
| gcip_iommu_reserve_manager_get_region_fit_locked(struct gcip_iommu_reserve_manager *mgr, |
| dma_addr_t start, size_t size) |
| { |
| struct gcip_iommu_reserve_region *region; |
| struct rb_node *node; |
| dma_addr_t end = start + size, region_end; |
| |
| lockdep_assert_held(&mgr->lock); |
| |
| if (end <= start) |
| return ERR_PTR(-EINVAL); |
| |
| node = mgr->regions.rb_node; |
| |
| while (node) { |
| region = rb_entry(node, struct gcip_iommu_reserve_region, node); |
| region_end = region->base_daddr + region->size; |
| |
| if (region->base_daddr <= start) { |
| if (region_end >= end) { |
| /* Found. @region can cover the buffer. */ |
| gcip_iommu_reserve_region_get(region); |
| return region; |
| } else if (region_end <= start) { |
| /* @region locates too left from the buffer. Try the right child. */ |
| node = node->rb_right; |
| continue; |
| } |
| } else if (region->base_daddr >= end) { |
| /* @region locates too right from the buffer. Try the left child. */ |
| node = node->rb_left; |
| continue; |
| } |
| |
| /* |
| * The region and the buffer are overlapped, but the region doesn't fully cover it. |
| * We can't proceed anymore. |
| */ |
| break; |
| } |
| |
| return ERR_PTR(-EINVAL); |
| } |
| |
| /* Retires @region if it is not retired yet. */ |
| static void gcip_iommu_reserve_region_try_retire(struct gcip_iommu_reserve_region *region) |
| { |
| struct gcip_iommu_reserve_mapping *cur, *tmp; |
| |
| mutex_lock(®ion->lock); |
| |
| /* This function is already called before. */ |
| if (region->retired) { |
| mutex_unlock(®ion->lock); |
| return; |
| } |
| |
| /* Makes @region->mappings won't be changed anymore. */ |
| region->retired = true; |
| |
| /* |
| * The `gcip_iommu_reserve_mapping_after_unmap` function still can be called while iterating |
| * @region->mappings below if the IP driver unmaps any mapping. Therefore, even though it is |
| * guaranteed that @region->mappings won't be changed anymore, the refcount of each mapping |
| * can be decremented in parallel. To prevent them from being released during the iteration, |
| * increments the refcount of them. |
| */ |
| list_for_each_entry(cur, ®ion->mappings, node) { |
| gcip_iommu_reserve_mapping_get(cur); |
| } |
| |
| mutex_unlock(®ion->lock); |
| |
| /* Unmaps all mappings which haven't unmapped yet. */ |
| list_for_each_entry_safe(cur, tmp, ®ion->mappings, node) { |
| list_del_init(&cur->node); |
| /* |
| * Calls @unmap callback instead of calling the `gcip_iommu_mapping_unmap` function |
| * directly because IP driver may access @cur->mapping even after this callback |
| * returns by the race condition. The IP driver will prepare their resources for |
| * unmapping the mapping immediately whenever they can guarantee that no one will |
| * access it anymore. The IP driver must call the `gcip_iommu_mapping_unmap` |
| * function by itself at that moment. |
| */ |
| region->mgr->ops->unmap(region->mgr, cur->mapping, cur->data); |
| /* |
| * Releases the refcount which was held above. If the mapping was actually unmapped, |
| * @cur will be released right away. |
| */ |
| gcip_iommu_reserve_mapping_put(cur); |
| } |
| } |
| |
| void gcip_iommu_reserve_manager_retire(struct gcip_iommu_reserve_manager *mgr) |
| { |
| struct gcip_iommu_reserve_region *region; |
| struct rb_node *node; |
| |
| mutex_lock(&mgr->lock); |
| |
| /* This function is already called before. */ |
| if (mgr->retired) { |
| mutex_unlock(&mgr->lock); |
| return; |
| } |
| |
| /* |
| * From now on, this is the only function which can retire regions of @mgr. It is guaranteed |
| * that @mgr->regions won't be changed, but also each region won't be released while |
| * iterating @mgr->regions below. |
| */ |
| mgr->retired = true; |
| |
| mutex_unlock(&mgr->lock); |
| |
| /* If there are regions which are not yet retired, retires all of them. */ |
| while ((node = rb_first(&mgr->regions))) { |
| region = rb_entry(node, struct gcip_iommu_reserve_region, node); |
| /* Retires @region. */ |
| gcip_iommu_reserve_region_try_retire(region); |
| /* |
| * Removes @region from @mgr->regions and decrements the refcount of @region. If |
| * there are no mappings which aren't yet unmapped from @region, @region will be |
| * released. |
| */ |
| gcip_iommu_reserve_manager_remove_region(mgr, region); |
| } |
| |
| gcip_iommu_reserve_manager_put(mgr); |
| } |
| |
| dma_addr_t gcip_iommu_reserve_region_create(struct gcip_iommu_reserve_manager *mgr, size_t size, |
| u64 gcip_map_flags) |
| { |
| struct gcip_iommu_reserve_region *region; |
| struct device *dev = mgr->domain->dev; |
| int ret; |
| |
| if (!size) |
| return 0; |
| |
| mutex_lock(&mgr->lock); |
| |
| if (mgr->retired) { |
| dev_err(dev, "The IOMMU reserve manager is already retired"); |
| goto err_out; |
| } |
| |
| region = kzalloc(sizeof(*region), GFP_KERNEL); |
| if (!region) |
| goto err_out; |
| |
| region->base_daddr = gcip_iommu_alloc_iova(mgr->domain, size, gcip_map_flags); |
| if (!region->base_daddr) { |
| dev_err(dev, "The domain doesn't have enough space to reserve a region, size=%zu", |
| size); |
| goto err_kfree; |
| } |
| |
| /* @mgr must not be released until @region is released. */ |
| gcip_iommu_reserve_manager_get(mgr); |
| |
| region->mgr = mgr; |
| region->domain = mgr->domain; |
| region->size = size; |
| mutex_init(®ion->lock); |
| kref_init(®ion->kref); |
| INIT_LIST_HEAD(®ion->mappings); |
| |
| /* |
| * Puts @region to @mgr->regions and increments the refcount of @region. It will be reverted |
| * when @region is retired and the `gcip_iommu_reserve_manager_remove_region` function is |
| * called. |
| */ |
| ret = gcip_iommu_reserve_manager_add_region_locked(mgr, region); |
| if (ret) { |
| dev_err(dev, "Failed to add the reserved region to the manager (ret=%d)", ret); |
| goto err_put_mgr; |
| } |
| |
| /* We can decrement the refcount as @mgr->regions is holding it. */ |
| gcip_iommu_reserve_region_put(region); |
| |
| mutex_unlock(&mgr->lock); |
| return region->base_daddr; |
| |
| err_put_mgr: |
| gcip_iommu_reserve_manager_put(mgr); |
| gcip_iommu_free_iova(mgr->domain, region->base_daddr, size); |
| err_kfree: |
| kfree(region); |
| err_out: |
| mutex_unlock(&mgr->lock); |
| return 0; |
| } |
| |
| int gcip_iommu_reserve_region_retire(struct gcip_iommu_reserve_manager *mgr, dma_addr_t base_daddr) |
| { |
| struct gcip_iommu_reserve_region *region; |
| |
| mutex_lock(&mgr->lock); |
| |
| /* |
| * @mgr is retired. all regions must be already or being retired by the |
| * `gcip_iommu_reserve_manager_retire` function. |
| */ |
| if (mgr->retired) { |
| mutex_unlock(&mgr->lock); |
| return -EPERM; |
| } |
| |
| /* Finds the region which starts at @base_daddr and increments its refcount. */ |
| region = gcip_iommu_reserve_manager_get_region_locked(mgr, base_daddr); |
| if (IS_ERR(region)) { |
| mutex_unlock(&mgr->lock); |
| return PTR_ERR(region); |
| } |
| |
| /* Removes @region from @mgr->regions and decrements the refcount of @region. */ |
| gcip_iommu_reserve_manager_remove_region(mgr, region); |
| |
| mutex_unlock(&mgr->lock); |
| |
| /* Retires @region. */ |
| gcip_iommu_reserve_region_try_retire(region); |
| |
| /* |
| * Decrements the refcount which was held by the `get_region` function above. If there are |
| * no mappings which aren't yet unmapped from @region, @region will be released. |
| */ |
| gcip_iommu_reserve_region_put(region); |
| |
| return 0; |
| } |
| |
| struct gcip_iommu_mapping *gcip_iommu_reserve_map_buffer(struct gcip_iommu_reserve_manager *mgr, |
| u64 host_address, size_t size, |
| u64 gcip_map_flags, |
| struct mutex *pin_user_pages_lock, |
| dma_addr_t iova, void *data) |
| { |
| struct gcip_iommu_reserve_region *region; |
| struct gcip_iommu_reserve_mapping *reserve_mapping; |
| struct gcip_iommu_mapping *mapping; |
| int ret; |
| |
| if (!size) |
| return ERR_PTR(-EINVAL); |
| |
| mutex_lock(&mgr->lock); |
| |
| if (mgr->retired) { |
| mutex_unlock(&mgr->lock); |
| return ERR_PTR(-EPERM); |
| } |
| |
| region = gcip_iommu_reserve_manager_get_region_fit_locked(mgr, iova, size); |
| mutex_unlock(&mgr->lock); |
| |
| if (IS_ERR(region)) |
| return ERR_CAST(region); |
| |
| mutex_lock(®ion->lock); |
| |
| if (region->retired) { |
| ret = -EPERM; |
| goto err_out; |
| } |
| |
| mapping = gcip_iommu_domain_map_buffer_to_iova(region->domain, host_address, size, iova, |
| gcip_map_flags, pin_user_pages_lock); |
| if (IS_ERR(mapping)) { |
| ret = PTR_ERR(mapping); |
| goto err_out; |
| } |
| |
| reserve_mapping = gcip_iommu_reserve_mapping_alloc_locked(region, mapping, data); |
| if (IS_ERR(reserve_mapping)) { |
| ret = PTR_ERR(reserve_mapping); |
| goto err_unmap; |
| } |
| |
| list_add_tail(&reserve_mapping->node, ®ion->mappings); |
| |
| mutex_unlock(®ion->lock); |
| gcip_iommu_reserve_region_put(region); |
| |
| return mapping; |
| |
| err_unmap: |
| gcip_iommu_mapping_unmap(mapping); |
| err_out: |
| mutex_unlock(®ion->lock); |
| gcip_iommu_reserve_region_put(region); |
| return ERR_PTR(ret); |
| } |
| |
| struct gcip_iommu_mapping *gcip_iommu_reserve_map_dma_buf(struct gcip_iommu_reserve_manager *mgr, |
| struct dma_buf *dmabuf, |
| u64 gcip_map_flags, dma_addr_t iova, |
| void *data) |
| { |
| struct gcip_iommu_reserve_region *region; |
| struct gcip_iommu_reserve_mapping *reserve_mapping; |
| struct gcip_iommu_mapping *mapping; |
| int ret; |
| |
| if (!dmabuf || !dmabuf->size) |
| return ERR_PTR(-EINVAL); |
| |
| mutex_lock(&mgr->lock); |
| |
| if (mgr->retired) { |
| mutex_unlock(&mgr->lock); |
| return ERR_PTR(-EPERM); |
| } |
| |
| region = gcip_iommu_reserve_manager_get_region_fit_locked(mgr, iova, dmabuf->size); |
| mutex_unlock(&mgr->lock); |
| |
| if (IS_ERR(region)) |
| return ERR_CAST(region); |
| |
| mutex_lock(®ion->lock); |
| |
| if (region->retired) { |
| ret = -EPERM; |
| goto err_out; |
| } |
| |
| mapping = |
| gcip_iommu_domain_map_dma_buf_to_iova(region->domain, dmabuf, iova, gcip_map_flags); |
| if (IS_ERR(mapping)) { |
| ret = PTR_ERR(mapping); |
| goto err_out; |
| } |
| |
| reserve_mapping = gcip_iommu_reserve_mapping_alloc_locked(region, mapping, data); |
| if (IS_ERR(reserve_mapping)) { |
| ret = PTR_ERR(reserve_mapping); |
| goto err_unmap; |
| } |
| |
| list_add_tail(&reserve_mapping->node, ®ion->mappings); |
| |
| mutex_unlock(®ion->lock); |
| gcip_iommu_reserve_region_put(region); |
| |
| return mapping; |
| |
| err_unmap: |
| gcip_iommu_mapping_unmap(mapping); |
| err_out: |
| mutex_unlock(®ion->lock); |
| gcip_iommu_reserve_region_put(region); |
| return ERR_PTR(ret); |
| } |