| /* |
| * |
| * Copyright (C) 2013 Google, Inc. |
| * |
| * This software is licensed under the terms of the GNU General Public |
| * License version 2, as published by the Free Software Foundation, and |
| * may be copied, distributed, and modified under those terms. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| */ |
| |
| #define pr_fmt(fmt) "ion-test: " fmt |
| |
| #include <linux/dma-buf.h> |
| #include <linux/dma-direction.h> |
| #include <linux/fs.h> |
| #include <linux/miscdevice.h> |
| #include <linux/mm.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/sched.h> |
| #include <linux/slab.h> |
| #include <linux/uaccess.h> |
| #include <linux/vmalloc.h> |
| |
| #include "ion.h" |
| #include "../uapi/ion_test.h" |
| |
| #define u64_to_uptr(x) ((void __user *)(unsigned long)(x)) |
| |
| struct ion_test_device { |
| struct miscdevice misc; |
| }; |
| |
| struct ion_test_data { |
| struct dma_buf *dma_buf; |
| struct device *dev; |
| }; |
| |
| static int ion_handle_test_dma(struct device *dev, struct dma_buf *dma_buf, |
| void __user *ptr, size_t offset, size_t size, bool write) |
| { |
| int ret = 0; |
| struct dma_buf_attachment *attach; |
| struct sg_table *table; |
| pgprot_t pgprot = pgprot_writecombine(PAGE_KERNEL); |
| enum dma_data_direction dir = write ? DMA_FROM_DEVICE : DMA_TO_DEVICE; |
| struct sg_page_iter sg_iter; |
| unsigned long offset_page; |
| |
| attach = dma_buf_attach(dma_buf, dev); |
| if (IS_ERR(attach)) |
| return PTR_ERR(attach); |
| |
| table = dma_buf_map_attachment(attach, dir); |
| if (IS_ERR(table)) |
| return PTR_ERR(table); |
| |
| offset_page = offset >> PAGE_SHIFT; |
| offset %= PAGE_SIZE; |
| |
| for_each_sg_page(table->sgl, &sg_iter, table->nents, offset_page) { |
| struct page *page = sg_page_iter_page(&sg_iter); |
| void *vaddr = vmap(&page, 1, VM_MAP, pgprot); |
| size_t to_copy = PAGE_SIZE - offset; |
| |
| to_copy = min(to_copy, size); |
| if (!vaddr) { |
| ret = -ENOMEM; |
| goto err; |
| } |
| |
| if (write) |
| ret = copy_from_user(vaddr + offset, ptr, to_copy); |
| else |
| ret = copy_to_user(ptr, vaddr + offset, to_copy); |
| |
| vunmap(vaddr); |
| if (ret) { |
| ret = -EFAULT; |
| goto err; |
| } |
| size -= to_copy; |
| if (!size) |
| break; |
| ptr += to_copy; |
| offset = 0; |
| } |
| |
| err: |
| dma_buf_unmap_attachment(attach, table, dir); |
| dma_buf_detach(dma_buf, attach); |
| return ret; |
| } |
| |
| static int ion_handle_test_kernel(struct dma_buf *dma_buf, void __user *ptr, |
| size_t offset, size_t size, bool write) |
| { |
| int ret; |
| unsigned long page_offset = offset >> PAGE_SHIFT; |
| size_t copy_offset = offset % PAGE_SIZE; |
| size_t copy_size = size; |
| enum dma_data_direction dir = write ? DMA_FROM_DEVICE : DMA_TO_DEVICE; |
| |
| if (offset > dma_buf->size || size > dma_buf->size - offset) |
| return -EINVAL; |
| |
| ret = dma_buf_begin_cpu_access(dma_buf, offset, size, dir); |
| if (ret) |
| return ret; |
| |
| while (copy_size > 0) { |
| size_t to_copy; |
| void *vaddr = dma_buf_kmap(dma_buf, page_offset); |
| |
| if (!vaddr) |
| goto err; |
| |
| to_copy = min_t(size_t, PAGE_SIZE - copy_offset, copy_size); |
| |
| if (write) |
| ret = copy_from_user(vaddr + copy_offset, ptr, to_copy); |
| else |
| ret = copy_to_user(ptr, vaddr + copy_offset, to_copy); |
| |
| dma_buf_kunmap(dma_buf, page_offset, vaddr); |
| if (ret) { |
| ret = -EFAULT; |
| goto err; |
| } |
| |
| copy_size -= to_copy; |
| ptr += to_copy; |
| page_offset++; |
| copy_offset = 0; |
| } |
| err: |
| dma_buf_end_cpu_access(dma_buf, offset, size, dir); |
| return ret; |
| } |
| |
| static long ion_test_ioctl(struct file *filp, unsigned int cmd, |
| unsigned long arg) |
| { |
| struct ion_test_data *test_data = filp->private_data; |
| int ret = 0; |
| |
| union { |
| struct ion_test_rw_data test_rw; |
| } data; |
| |
| if (_IOC_SIZE(cmd) > sizeof(data)) |
| return -EINVAL; |
| |
| if (_IOC_DIR(cmd) & _IOC_WRITE) |
| if (copy_from_user(&data, (void __user *)arg, _IOC_SIZE(cmd))) |
| return -EFAULT; |
| |
| switch (cmd) { |
| case ION_IOC_TEST_SET_FD: |
| { |
| struct dma_buf *dma_buf = NULL; |
| int fd = arg; |
| |
| if (fd >= 0) { |
| dma_buf = dma_buf_get((int)arg); |
| if (IS_ERR(dma_buf)) |
| return PTR_ERR(dma_buf); |
| } |
| if (test_data->dma_buf) |
| dma_buf_put(test_data->dma_buf); |
| test_data->dma_buf = dma_buf; |
| break; |
| } |
| case ION_IOC_TEST_DMA_MAPPING: |
| { |
| ret = ion_handle_test_dma(test_data->dev, test_data->dma_buf, |
| u64_to_uptr(data.test_rw.ptr), |
| data.test_rw.offset, data.test_rw.size, |
| data.test_rw.write); |
| break; |
| } |
| case ION_IOC_TEST_KERNEL_MAPPING: |
| { |
| ret = ion_handle_test_kernel(test_data->dma_buf, |
| u64_to_uptr(data.test_rw.ptr), |
| data.test_rw.offset, data.test_rw.size, |
| data.test_rw.write); |
| break; |
| } |
| default: |
| return -ENOTTY; |
| } |
| |
| if (_IOC_DIR(cmd) & _IOC_READ) { |
| if (copy_to_user((void __user *)arg, &data, sizeof(data))) |
| return -EFAULT; |
| } |
| return ret; |
| } |
| |
| static int ion_test_open(struct inode *inode, struct file *file) |
| { |
| struct ion_test_data *data; |
| struct miscdevice *miscdev = file->private_data; |
| |
| data = kzalloc(sizeof(struct ion_test_data), GFP_KERNEL); |
| if (!data) |
| return -ENOMEM; |
| |
| data->dev = miscdev->parent; |
| |
| file->private_data = data; |
| |
| return 0; |
| } |
| |
| static int ion_test_release(struct inode *inode, struct file *file) |
| { |
| struct ion_test_data *data = file->private_data; |
| |
| kfree(data); |
| |
| return 0; |
| } |
| |
| static const struct file_operations ion_test_fops = { |
| .owner = THIS_MODULE, |
| .unlocked_ioctl = ion_test_ioctl, |
| .compat_ioctl = ion_test_ioctl, |
| .open = ion_test_open, |
| .release = ion_test_release, |
| }; |
| |
| static int __init ion_test_probe(struct platform_device *pdev) |
| { |
| int ret; |
| struct ion_test_device *testdev; |
| |
| testdev = devm_kzalloc(&pdev->dev, sizeof(struct ion_test_device), |
| GFP_KERNEL); |
| if (!testdev) |
| return -ENOMEM; |
| |
| testdev->misc.minor = MISC_DYNAMIC_MINOR; |
| testdev->misc.name = "ion-test"; |
| testdev->misc.fops = &ion_test_fops; |
| testdev->misc.parent = &pdev->dev; |
| ret = misc_register(&testdev->misc); |
| if (ret) { |
| pr_err("failed to register misc device.\n"); |
| return ret; |
| } |
| |
| platform_set_drvdata(pdev, testdev); |
| |
| return 0; |
| } |
| |
| static int ion_test_remove(struct platform_device *pdev) |
| { |
| struct ion_test_device *testdev; |
| |
| testdev = platform_get_drvdata(pdev); |
| if (!testdev) |
| return -ENODATA; |
| |
| misc_deregister(&testdev->misc); |
| return 0; |
| } |
| |
| static struct platform_device *ion_test_pdev; |
| static struct platform_driver ion_test_platform_driver = { |
| .remove = ion_test_remove, |
| .driver = { |
| .name = "ion-test", |
| }, |
| }; |
| |
| static int __init ion_test_init(void) |
| { |
| ion_test_pdev = platform_device_register_simple("ion-test", |
| -1, NULL, 0); |
| if (!ion_test_pdev) |
| return -ENODEV; |
| |
| return platform_driver_probe(&ion_test_platform_driver, ion_test_probe); |
| } |
| |
| static void __exit ion_test_exit(void) |
| { |
| platform_driver_unregister(&ion_test_platform_driver); |
| platform_device_unregister(ion_test_pdev); |
| } |
| |
| module_init(ion_test_init); |
| module_exit(ion_test_exit); |
| MODULE_LICENSE("GPL v2"); |