| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2020 Google, Inc. |
| */ |
| |
| #include <linux/ctype.h> |
| #include <linux/list.h> |
| #include <linux/platform_device.h> |
| #include <linux/trusty/smcall.h> |
| #include <linux/trusty/trusty.h> |
| #include <linux/scatterlist.h> |
| #include <linux/slab.h> |
| #include <linux/mm.h> |
| #include <linux/mod_devicetable.h> |
| #include <linux/module.h> |
| |
| #include "trusty-test.h" |
| |
| struct trusty_test_state { |
| struct device *dev; |
| struct device *trusty_dev; |
| }; |
| |
| struct trusty_test_shmem_obj { |
| struct list_head node; |
| size_t page_count; |
| struct page **pages; |
| void *buf; |
| struct sg_table sgt; |
| trusty_shared_mem_id_t mem_id; |
| }; |
| |
| /* |
| * Allocate a test object with @page_count number of pages, map it and add it to |
| * @list. |
| * For multi-page allocations, order the pages so they are not contiguous. |
| */ |
| static int trusty_test_alloc_obj(struct trusty_test_state *s, |
| size_t page_count, |
| struct list_head *list) |
| { |
| size_t i; |
| int ret = -ENOMEM; |
| struct trusty_test_shmem_obj *obj; |
| |
| obj = kzalloc(sizeof(*obj), GFP_KERNEL); |
| if (!obj) |
| goto err_alloc_obj; |
| obj->page_count = page_count; |
| |
| obj->pages = kmalloc_array(page_count, sizeof(*obj->pages), GFP_KERNEL); |
| if (!obj->pages) { |
| ret = -ENOMEM; |
| dev_err(s->dev, "failed to allocate page array, count %zd\n", |
| page_count); |
| goto err_alloc_pages; |
| } |
| |
| for (i = 0; i < page_count; i++) { |
| obj->pages[i] = alloc_page(GFP_KERNEL); |
| if (!obj->pages[i]) { |
| ret = -ENOMEM; |
| dev_err(s->dev, "failed to allocate page %zd/%zd\n", |
| i, page_count); |
| goto err_alloc_page; |
| } |
| if (i > 0 && obj->pages[i - 1] + 1 == obj->pages[i]) { |
| /* swap adacent pages to increase fragmentation */ |
| swap(obj->pages[i - 1], obj->pages[i]); |
| } |
| } |
| |
| obj->buf = vmap(obj->pages, page_count, VM_MAP, PAGE_KERNEL); |
| if (!obj->buf) { |
| ret = -ENOMEM; |
| dev_err(s->dev, "failed to map test buffer page count %zd\n", |
| page_count); |
| goto err_map_pages; |
| } |
| |
| ret = sg_alloc_table_from_pages(&obj->sgt, obj->pages, page_count, |
| 0, page_count * PAGE_SIZE, GFP_KERNEL); |
| if (ret) { |
| dev_err(s->dev, "sg_alloc_table_from_pages failed: %d\n", ret); |
| goto err_alloc_sgt; |
| } |
| list_add_tail(&obj->node, list); |
| dev_dbg(s->dev, "buffer has %d page runs\n", obj->sgt.nents); |
| return 0; |
| |
| err_alloc_sgt: |
| vunmap(obj->buf); |
| err_map_pages: |
| for (i = page_count; i > 0; i--) { |
| __free_page(obj->pages[i - 1]); |
| err_alloc_page: |
| ; |
| } |
| kfree(obj->pages); |
| err_alloc_pages: |
| kfree(obj); |
| err_alloc_obj: |
| return ret; |
| } |
| |
| /* Unlink, unmap and free a test object and its pages */ |
| static void trusty_test_free_obj(struct trusty_test_state *s, |
| struct trusty_test_shmem_obj *obj) |
| { |
| size_t i; |
| |
| list_del(&obj->node); |
| sg_free_table(&obj->sgt); |
| vunmap(obj->buf); |
| for (i = obj->page_count; i > 0; i--) |
| __free_page(obj->pages[i - 1]); |
| kfree(obj->pages); |
| kfree(obj); |
| } |
| |
| /* |
| * Share all the pages of all the test object in &obj_list. |
| * If sharing a test object fails, free it so that every test object that |
| * remains in @obj_list has been shared when this function returns. |
| * Return a error if any test object failed to be shared. |
| */ |
| static int trusty_test_share_objs(struct trusty_test_state *s, |
| struct list_head *obj_list, size_t size) |
| { |
| int ret = 0; |
| int tmpret; |
| struct trusty_test_shmem_obj *obj; |
| struct trusty_test_shmem_obj *next_obj; |
| ktime_t t1; |
| ktime_t t2; |
| |
| list_for_each_entry_safe(obj, next_obj, obj_list, node) { |
| t1 = ktime_get(); |
| tmpret = trusty_share_memory(s->trusty_dev, &obj->mem_id, |
| obj->sgt.sgl, obj->sgt.nents, |
| PAGE_KERNEL); |
| t2 = ktime_get(); |
| if (tmpret) { |
| ret = tmpret; |
| dev_err(s->dev, |
| "trusty_share_memory failed: %d, size=%zd\n", |
| ret, size); |
| |
| /* |
| * Free obj and continue, so we can revoke the |
| * whole list in trusty_test_reclaim_objs. |
| */ |
| trusty_test_free_obj(s, obj); |
| } |
| dev_dbg(s->dev, "share id=0x%llx, size=%zu took %lld ns\n", |
| obj->mem_id, size, |
| ktime_to_ns(ktime_sub(t2, t1))); |
| } |
| |
| return ret; |
| } |
| |
| /* Reclaim memory shared with trusty for all test objects in @obj_list. */ |
| static int trusty_test_reclaim_objs(struct trusty_test_state *s, |
| struct list_head *obj_list, size_t size) |
| { |
| int ret = 0; |
| int tmpret; |
| struct trusty_test_shmem_obj *obj; |
| struct trusty_test_shmem_obj *next_obj; |
| ktime_t t1; |
| ktime_t t2; |
| |
| list_for_each_entry_safe(obj, next_obj, obj_list, node) { |
| t1 = ktime_get(); |
| tmpret = trusty_reclaim_memory(s->trusty_dev, obj->mem_id, |
| obj->sgt.sgl, obj->sgt.nents); |
| t2 = ktime_get(); |
| if (tmpret) { |
| ret = tmpret; |
| dev_err(s->dev, |
| "trusty_reclaim_memory failed: %d, id=0x%llx\n", |
| ret, obj->mem_id); |
| |
| /* |
| * It is not safe to free this memory if |
| * trusty_reclaim_memory fails. Leak it in that |
| * case. |
| */ |
| list_del(&obj->node); |
| } |
| dev_dbg(s->dev, "revoke id=0x%llx, size=%zu took %lld ns\n", |
| obj->mem_id, size, |
| ktime_to_ns(ktime_sub(t2, t1))); |
| } |
| |
| return ret; |
| } |
| |
| /* |
| * Test a test object. First, initialize the memory, then make a std call into |
| * trusty which will read it and return an error if the initialized value does |
| * not match what it expects. If trusty reads the correct values, it will modify |
| * the memory and return 0. This function then checks that it can read the |
| * correct modified value. |
| */ |
| static int trusty_test_rw(struct trusty_test_state *s, |
| struct trusty_test_shmem_obj *obj) |
| { |
| size_t size = obj->page_count * PAGE_SIZE; |
| int ret; |
| size_t i; |
| u64 *buf = obj->buf; |
| ktime_t t1; |
| ktime_t t2; |
| |
| for (i = 0; i < size / sizeof(*buf); i++) |
| buf[i] = i; |
| |
| t1 = ktime_get(); |
| ret = trusty_std_call32(s->trusty_dev, SMC_SC_TEST_SHARED_MEM_RW, |
| (u32)(obj->mem_id), (u32)(obj->mem_id >> 32), |
| size); |
| t2 = ktime_get(); |
| if (ret < 0) { |
| dev_err(s->dev, |
| "trusty std call (SMC_SC_TEST_SHARED_MEM_RW) failed: %d 0x%llx\n", |
| ret, obj->mem_id); |
| return ret; |
| } |
| |
| for (i = 0; i < size / sizeof(*buf); i++) { |
| if (buf[i] != size - i) { |
| dev_err(s->dev, |
| "input mismatch at %zd, got 0x%llx instead of 0x%zx\n", |
| i, buf[i], size - i); |
| return -EIO; |
| } |
| } |
| |
| dev_dbg(s->dev, "rw id=0x%llx, size=%zu took %lld ns\n", obj->mem_id, |
| size, ktime_to_ns(ktime_sub(t2, t1))); |
| |
| return 0; |
| } |
| |
| /* |
| * Run test on every test object in @obj_list. Repeat @repeat_access times. |
| */ |
| static int trusty_test_rw_objs(struct trusty_test_state *s, |
| struct list_head *obj_list, |
| size_t repeat_access) |
| { |
| int ret; |
| size_t i; |
| struct trusty_test_shmem_obj *obj; |
| |
| for (i = 0; i < repeat_access; i++) { |
| /* |
| * Repeat test in case the memory attributes don't match |
| * and either side see old data. |
| */ |
| list_for_each_entry(obj, obj_list, node) { |
| ret = trusty_test_rw(s, obj); |
| if (ret) |
| return ret; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Allocate @obj_count test object that each have @page_count pages. Share each |
| * object @repeat_share times, each time running tests on every object |
| * @repeat_access times. |
| */ |
| static int trusty_test_run(struct trusty_test_state *s, size_t page_count, |
| size_t obj_count, size_t repeat_share, |
| size_t repeat_access) |
| { |
| int ret = 0; |
| int tmpret; |
| size_t i; |
| size_t size = page_count * PAGE_SIZE; |
| LIST_HEAD(obj_list); |
| struct trusty_test_shmem_obj *obj; |
| struct trusty_test_shmem_obj *next_obj; |
| |
| for (i = 0; i < obj_count && !ret; i++) |
| ret = trusty_test_alloc_obj(s, page_count, &obj_list); |
| |
| for (i = 0; i < repeat_share && !ret; i++) { |
| ret = trusty_test_share_objs(s, &obj_list, size); |
| if (ret) { |
| dev_err(s->dev, |
| "trusty_share_memory failed: %d, i=%zd/%zd, size=%zd\n", |
| ret, i, repeat_share, size); |
| } else { |
| ret = trusty_test_rw_objs(s, &obj_list, repeat_access); |
| if (ret) |
| dev_err(s->dev, |
| "test failed: %d, i=%zd/%zd, size=%zd\n", |
| ret, i, repeat_share, size); |
| } |
| tmpret = trusty_test_reclaim_objs(s, &obj_list, size); |
| if (tmpret) { |
| ret = tmpret; |
| dev_err(s->dev, |
| "trusty_reclaim_memory failed: %d, i=%zd/%zd\n", |
| ret, i, repeat_share); |
| } |
| } |
| |
| list_for_each_entry_safe(obj, next_obj, &obj_list, node) |
| trusty_test_free_obj(s, obj); |
| |
| dev_info(s->dev, "[ %s ] size %zd, obj_count %zd, repeat_share %zd, repeat_access %zd\n", |
| ret ? "FAILED" : "PASSED", size, obj_count, repeat_share, |
| repeat_access); |
| |
| return ret; |
| } |
| |
| /* |
| * Get an optional numeric argument from @buf, update @buf and return the value. |
| * If @buf does not start with ",", return @default_val instead. |
| */ |
| static size_t trusty_test_get_arg(const char **buf, size_t default_val) |
| { |
| char *buf_next; |
| size_t ret; |
| |
| if (**buf != ',') |
| return default_val; |
| |
| (*buf)++; |
| ret = simple_strtoul(*buf, &buf_next, 0); |
| if (buf_next == *buf) |
| return default_val; |
| |
| *buf = buf_next; |
| |
| return ret; |
| } |
| |
| /* |
| * Run tests described by a string in this format: |
| * <obj_size>,<obj_count=1>,<repeat_share=1>,<repeat_access=3> |
| */ |
| static ssize_t trusty_test_run_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct trusty_test_state *s = platform_get_drvdata(pdev); |
| size_t size; |
| size_t obj_count; |
| size_t repeat_share; |
| size_t repeat_access; |
| int ret; |
| char *buf_next; |
| |
| while (true) { |
| while (isspace(*buf)) |
| buf++; |
| size = simple_strtoul(buf, &buf_next, 0); |
| if (buf_next == buf) |
| return count; |
| buf = buf_next; |
| obj_count = trusty_test_get_arg(&buf, 1); |
| repeat_share = trusty_test_get_arg(&buf, 1); |
| repeat_access = trusty_test_get_arg(&buf, 3); |
| |
| ret = trusty_test_run(s, DIV_ROUND_UP(size, PAGE_SIZE), |
| obj_count, repeat_share, repeat_access); |
| if (ret) |
| return ret; |
| } |
| } |
| |
| static DEVICE_ATTR_WO(trusty_test_run); |
| |
| static struct attribute *trusty_test_attrs[] = { |
| &dev_attr_trusty_test_run.attr, |
| NULL, |
| }; |
| ATTRIBUTE_GROUPS(trusty_test); |
| |
| static int trusty_test_probe(struct platform_device *pdev) |
| { |
| struct trusty_test_state *s; |
| int ret; |
| |
| ret = trusty_std_call32(pdev->dev.parent, SMC_SC_TEST_VERSION, |
| TRUSTY_STDCALLTEST_API_VERSION, 0, 0); |
| if (ret != TRUSTY_STDCALLTEST_API_VERSION) |
| return -ENOENT; |
| |
| s = kzalloc(sizeof(*s), GFP_KERNEL); |
| if (!s) |
| return -ENOMEM; |
| |
| s->dev = &pdev->dev; |
| s->trusty_dev = s->dev->parent; |
| |
| platform_set_drvdata(pdev, s); |
| |
| return 0; |
| } |
| |
| static int trusty_test_remove(struct platform_device *pdev) |
| { |
| struct trusty_log_state *s = platform_get_drvdata(pdev); |
| |
| kfree(s); |
| return 0; |
| } |
| |
| static const struct of_device_id trusty_test_of_match[] = { |
| { .compatible = "android,trusty-test-v1", }, |
| {}, |
| }; |
| |
| MODULE_DEVICE_TABLE(trusty, trusty_test_of_match); |
| |
| static struct platform_driver trusty_test_driver = { |
| .probe = trusty_test_probe, |
| .remove = trusty_test_remove, |
| .driver = { |
| .name = "trusty-test", |
| .of_match_table = trusty_test_of_match, |
| .dev_groups = trusty_test_groups, |
| }, |
| }; |
| |
| module_platform_driver(trusty_test_driver); |
| |
| MODULE_LICENSE("GPL v2"); |
| MODULE_DESCRIPTION("Trusty test driver"); |