| /* |
| * Copyright (c) 2015, The Linux Foundation. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 and |
| * only version 2 as published by the Free Software Foundation. |
| * |
| * 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) "iommu-debug: %s: " fmt, __func__ |
| |
| #include <linux/debugfs.h> |
| #include <linux/device.h> |
| #include <linux/iommu.h> |
| #include <linux/of.h> |
| #include <linux/platform_device.h> |
| #include <linux/slab.h> |
| #include <linux/uaccess.h> |
| #include <soc/qcom/secure_buffer.h> |
| #include <linux/qcom_iommu.h> |
| |
| #ifdef CONFIG_IOMMU_DEBUG_TRACKING |
| |
| static DEFINE_MUTEX(iommu_debug_attachments_lock); |
| static LIST_HEAD(iommu_debug_attachments); |
| static struct dentry *debugfs_attachments_dir; |
| |
| struct iommu_debug_attachment { |
| struct iommu_domain *domain; |
| struct device *dev; |
| struct dentry *dentry; |
| struct list_head list; |
| unsigned long reg_offset; |
| }; |
| |
| static int iommu_debug_attachment_info_show(struct seq_file *s, void *ignored) |
| { |
| struct iommu_debug_attachment *attach = s->private; |
| phys_addr_t pt_phys; |
| int coherent_htw_disable; |
| int secure_vmid; |
| |
| seq_printf(s, "Domain: 0x%p\n", attach->domain); |
| if (iommu_domain_get_attr(attach->domain, DOMAIN_ATTR_PT_BASE_ADDR, |
| &pt_phys)) { |
| seq_puts(s, "PT_BASE_ADDR: (Unknown)\n"); |
| } else { |
| void *pt_virt = phys_to_virt(pt_phys); |
| |
| seq_printf(s, "PT_BASE_ADDR: virt=0x%p phys=%pa\n", |
| pt_virt, &pt_phys); |
| } |
| |
| seq_puts(s, "COHERENT_HTW_DISABLE: "); |
| if (iommu_domain_get_attr(attach->domain, |
| DOMAIN_ATTR_COHERENT_HTW_DISABLE, |
| &coherent_htw_disable)) |
| seq_puts(s, "(Unknown)\n"); |
| else |
| seq_printf(s, "%d\n", coherent_htw_disable); |
| |
| seq_puts(s, "SECURE_VMID: "); |
| if (iommu_domain_get_attr(attach->domain, |
| DOMAIN_ATTR_SECURE_VMID, |
| &secure_vmid)) |
| seq_puts(s, "(Unknown)\n"); |
| else |
| seq_printf(s, "%s (0x%x)\n", |
| msm_secure_vmid_to_string(secure_vmid), secure_vmid); |
| |
| return 0; |
| } |
| |
| static int iommu_debug_attachment_info_open(struct inode *inode, |
| struct file *file) |
| { |
| return single_open(file, iommu_debug_attachment_info_show, |
| inode->i_private); |
| } |
| |
| static const struct file_operations iommu_debug_attachment_info_fops = { |
| .open = iommu_debug_attachment_info_open, |
| .read = seq_read, |
| .llseek = seq_lseek, |
| .release = single_release, |
| }; |
| |
| static ssize_t iommu_debug_attachment_trigger_fault_write( |
| struct file *file, const char __user *ubuf, size_t count, |
| loff_t *offset) |
| { |
| struct iommu_debug_attachment *attach = file->private_data; |
| unsigned long flags; |
| |
| if (kstrtoul_from_user(ubuf, count, 0, &flags)) { |
| pr_err("Invalid flags format\n"); |
| return -EFAULT; |
| } |
| |
| iommu_trigger_fault(attach->domain, flags); |
| |
| return count; |
| } |
| |
| static const struct file_operations |
| iommu_debug_attachment_trigger_fault_fops = { |
| .open = simple_open, |
| .write = iommu_debug_attachment_trigger_fault_write, |
| }; |
| |
| static ssize_t iommu_debug_attachment_reg_offset_write( |
| struct file *file, const char __user *ubuf, size_t count, |
| loff_t *offset) |
| { |
| struct iommu_debug_attachment *attach = file->private_data; |
| unsigned long reg_offset; |
| |
| if (kstrtoul_from_user(ubuf, count, 0, ®_offset)) { |
| pr_err("Invalid reg_offset format\n"); |
| return -EFAULT; |
| } |
| |
| attach->reg_offset = reg_offset; |
| |
| return count; |
| } |
| |
| static const struct file_operations iommu_debug_attachment_reg_offset_fops = { |
| .open = simple_open, |
| .write = iommu_debug_attachment_reg_offset_write, |
| }; |
| |
| static ssize_t iommu_debug_attachment_reg_read_read( |
| struct file *file, char __user *ubuf, size_t count, loff_t *offset) |
| { |
| struct iommu_debug_attachment *attach = file->private_data; |
| unsigned long val; |
| char *val_str; |
| ssize_t val_str_len; |
| |
| if (*offset) |
| return 0; |
| |
| val = iommu_reg_read(attach->domain, attach->reg_offset); |
| val_str = kasprintf(GFP_KERNEL, "0x%lx\n", val); |
| if (!val_str) |
| return -ENOMEM; |
| val_str_len = strlen(val_str); |
| |
| if (copy_to_user(ubuf, val_str, val_str_len)) { |
| pr_err("copy_to_user failed\n"); |
| val_str_len = -EFAULT; |
| goto out; |
| } |
| *offset = 1; /* non-zero means we're done */ |
| |
| out: |
| kfree(val_str); |
| return val_str_len; |
| } |
| |
| static const struct file_operations iommu_debug_attachment_reg_read_fops = { |
| .open = simple_open, |
| .read = iommu_debug_attachment_reg_read_read, |
| }; |
| |
| static ssize_t iommu_debug_attachment_reg_write_write( |
| struct file *file, const char __user *ubuf, size_t count, |
| loff_t *offset) |
| { |
| struct iommu_debug_attachment *attach = file->private_data; |
| unsigned long val; |
| |
| if (kstrtoul_from_user(ubuf, count, 0, &val)) { |
| pr_err("Invalid val format\n"); |
| return -EFAULT; |
| } |
| |
| iommu_reg_write(attach->domain, attach->reg_offset, val); |
| |
| return count; |
| } |
| |
| static const struct file_operations iommu_debug_attachment_reg_write_fops = { |
| .open = simple_open, |
| .write = iommu_debug_attachment_reg_write_write, |
| }; |
| |
| /* should be called with iommu_debug_attachments_lock locked */ |
| static int iommu_debug_attach_add_debugfs( |
| struct iommu_debug_attachment *attach) |
| { |
| const char *attach_name; |
| struct device *dev = attach->dev; |
| struct iommu_domain *domain = attach->domain; |
| int is_dynamic; |
| |
| if (iommu_domain_get_attr(domain, DOMAIN_ATTR_DYNAMIC, &is_dynamic)) |
| is_dynamic = 0; |
| |
| if (is_dynamic) { |
| uuid_le uuid; |
| |
| uuid_le_gen(&uuid); |
| attach_name = kasprintf(GFP_KERNEL, "%s-%pUl", dev_name(dev), |
| uuid.b); |
| if (!attach_name) |
| return -ENOMEM; |
| } else { |
| attach_name = dev_name(dev); |
| } |
| |
| attach->dentry = debugfs_create_dir(attach_name, |
| debugfs_attachments_dir); |
| if (!attach->dentry) { |
| pr_err("Couldn't create iommu/attachments/%s debugfs directory for domain 0x%p\n", |
| attach_name, domain); |
| if (is_dynamic) |
| kfree(attach_name); |
| return -EIO; |
| } |
| |
| if (is_dynamic) |
| kfree(attach_name); |
| |
| if (!debugfs_create_file( |
| "info", S_IRUSR, attach->dentry, attach, |
| &iommu_debug_attachment_info_fops)) { |
| pr_err("Couldn't create iommu/attachments/%s/info debugfs file for domain 0x%p\n", |
| dev_name(dev), domain); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file( |
| "trigger_fault", S_IRUSR, attach->dentry, attach, |
| &iommu_debug_attachment_trigger_fault_fops)) { |
| pr_err("Couldn't create iommu/attachments/%s/trigger_fault debugfs file for domain 0x%p\n", |
| dev_name(dev), domain); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file( |
| "reg_offset", S_IRUSR, attach->dentry, attach, |
| &iommu_debug_attachment_reg_offset_fops)) { |
| pr_err("Couldn't create iommu/attachments/%s/reg_offset debugfs file for domain 0x%p\n", |
| dev_name(dev), domain); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file( |
| "reg_read", S_IRUSR, attach->dentry, attach, |
| &iommu_debug_attachment_reg_read_fops)) { |
| pr_err("Couldn't create iommu/attachments/%s/reg_read debugfs file for domain 0x%p\n", |
| dev_name(dev), domain); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file( |
| "reg_write", S_IRUSR, attach->dentry, attach, |
| &iommu_debug_attachment_reg_write_fops)) { |
| pr_err("Couldn't create iommu/attachments/%s/reg_write debugfs file for domain 0x%p\n", |
| dev_name(dev), domain); |
| goto err_rmdir; |
| } |
| |
| return 0; |
| |
| err_rmdir: |
| debugfs_remove_recursive(attach->dentry); |
| return -EIO; |
| } |
| |
| void iommu_debug_attach_device(struct iommu_domain *domain, |
| struct device *dev) |
| { |
| struct iommu_debug_attachment *attach; |
| |
| mutex_lock(&iommu_debug_attachments_lock); |
| |
| attach = kmalloc(sizeof(*attach), GFP_KERNEL); |
| if (!attach) |
| goto out_unlock; |
| |
| attach->domain = domain; |
| attach->dev = dev; |
| |
| /* |
| * we might not init until after other drivers start calling |
| * iommu_attach_device. Only set up the debugfs nodes if we've |
| * already init'd to avoid polluting the top-level debugfs |
| * directory (by calling debugfs_create_dir with a NULL |
| * parent). These will be flushed out later once we init. |
| */ |
| if (debugfs_attachments_dir) |
| iommu_debug_attach_add_debugfs(attach); |
| |
| list_add(&attach->list, &iommu_debug_attachments); |
| out_unlock: |
| mutex_unlock(&iommu_debug_attachments_lock); |
| } |
| |
| void iommu_debug_detach_device(struct iommu_domain *domain, |
| struct device *dev) |
| { |
| struct iommu_debug_attachment *it; |
| |
| mutex_lock(&iommu_debug_attachments_lock); |
| list_for_each_entry(it, &iommu_debug_attachments, list) |
| if (it->domain == domain && it->dev == dev) |
| break; |
| |
| if (&it->list == &iommu_debug_attachments) { |
| WARN(1, "Couldn't find debug attachment for domain=0x%p dev=%s", |
| domain, dev_name(dev)); |
| } else { |
| list_del(&it->list); |
| debugfs_remove_recursive(it->dentry); |
| kfree(it); |
| } |
| mutex_unlock(&iommu_debug_attachments_lock); |
| } |
| |
| static int iommu_debug_init_tracking(void) |
| { |
| int ret = 0; |
| struct iommu_debug_attachment *attach; |
| |
| mutex_lock(&iommu_debug_attachments_lock); |
| debugfs_attachments_dir = debugfs_create_dir("attachments", |
| iommu_debugfs_top); |
| if (!debugfs_attachments_dir) { |
| pr_err("Couldn't create iommu/attachments debugfs directory\n"); |
| ret = -ENODEV; |
| goto out_unlock; |
| } |
| |
| /* set up debugfs entries for attachments made during early boot */ |
| list_for_each_entry(attach, &iommu_debug_attachments, list) |
| iommu_debug_attach_add_debugfs(attach); |
| |
| out_unlock: |
| mutex_unlock(&iommu_debug_attachments_lock); |
| return ret; |
| } |
| |
| static void iommu_debug_destroy_tracking(void) |
| { |
| debugfs_remove_recursive(debugfs_attachments_dir); |
| } |
| #else |
| static inline int iommu_debug_init_tracking(void) { return 0; } |
| static inline void iommu_debug_destroy_tracking(void) { } |
| #endif |
| |
| #ifdef CONFIG_IOMMU_TESTS |
| |
| static LIST_HEAD(iommu_debug_devices); |
| static struct dentry *debugfs_tests_dir; |
| static u32 iters_per_op = 1; |
| |
| struct iommu_debug_device { |
| struct device *dev; |
| struct iommu_domain *domain; |
| u64 iova; |
| u64 phys; |
| size_t len; |
| struct list_head list; |
| }; |
| |
| static int iommu_debug_build_phoney_sg_table(struct device *dev, |
| struct sg_table *table, |
| unsigned long total_size, |
| unsigned long chunk_size) |
| { |
| unsigned long nents = total_size / chunk_size; |
| struct scatterlist *sg; |
| int i; |
| struct page *page; |
| |
| BUG_ON(!IS_ALIGNED(total_size, PAGE_SIZE)); |
| BUG_ON(!IS_ALIGNED(total_size, chunk_size)); |
| BUG_ON(sg_alloc_table(table, nents, GFP_KERNEL)); |
| page = alloc_pages(GFP_KERNEL, get_order(chunk_size)); |
| if (!page) |
| goto free_table; |
| |
| /* all the same page... why not. */ |
| for_each_sg(table->sgl, sg, table->nents, i) |
| sg_set_page(sg, page, chunk_size, 0); |
| |
| return 0; |
| |
| free_table: |
| sg_free_table(table); |
| return -ENOMEM; |
| } |
| |
| static void iommu_debug_destroy_phoney_sg_table(struct device *dev, |
| struct sg_table *table, |
| unsigned long chunk_size) |
| { |
| __free_pages(sg_page(table->sgl), get_order(chunk_size)); |
| sg_free_table(table); |
| } |
| |
| static const char * const _size_to_string(unsigned long size) |
| { |
| switch (size) { |
| case SZ_4K: |
| return "4K"; |
| case SZ_64K: |
| return "64K"; |
| case SZ_2M: |
| return "2M"; |
| case SZ_1M * 12: |
| return "12M"; |
| case SZ_1M * 20: |
| return "20M"; |
| } |
| return "unknown size, please add to _size_to_string"; |
| } |
| |
| static int nr_iters_set(void *data, u64 val) |
| { |
| if (!val) |
| val = 1; |
| if (val > 10000) |
| val = 10000; |
| *(u32 *)data = val; |
| return 0; |
| } |
| |
| static int nr_iters_get(void *data, u64 *val) |
| { |
| *val = *(u32 *)data; |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(iommu_debug_nr_iters_ops, |
| nr_iters_get, nr_iters_set, "%llu\n"); |
| |
| static void iommu_debug_device_profiling(struct seq_file *s, struct device *dev, |
| bool secure) |
| { |
| unsigned long sizes[] = { SZ_4K, SZ_64K, SZ_2M, SZ_1M * 12, |
| SZ_1M * 20, 0 }; |
| unsigned long *sz; |
| struct iommu_domain *domain; |
| struct bus_type *bus; |
| unsigned long iova = 0x10000; |
| phys_addr_t paddr = 0xa000; |
| int htw_disable = 1, atomic_domain = 1; |
| |
| bus = msm_iommu_get_bus(dev); |
| if (!bus) |
| return; |
| |
| domain = iommu_domain_alloc(bus); |
| if (!domain) { |
| seq_puts(s, "Couldn't allocate domain\n"); |
| return; |
| } |
| |
| if (iommu_domain_set_attr(domain, DOMAIN_ATTR_COHERENT_HTW_DISABLE, |
| &htw_disable)) { |
| seq_puts(s, "Couldn't disable coherent htw\n"); |
| goto out_domain_free; |
| } |
| |
| if (iommu_domain_set_attr(domain, DOMAIN_ATTR_ATOMIC, |
| &atomic_domain)) { |
| seq_printf(s, "Couldn't set atomic_domain to %d\n", |
| atomic_domain); |
| goto out_domain_free; |
| } |
| |
| if (secure) { |
| int secure_vmid = VMID_CP_PIXEL; |
| |
| if (iommu_domain_set_attr(domain, DOMAIN_ATTR_SECURE_VMID, |
| &secure_vmid)) { |
| seq_printf(s, "Couldn't set secure vmid to %d\n", |
| secure_vmid); |
| goto out_domain_free; |
| } |
| } |
| |
| if (iommu_attach_device(domain, dev)) { |
| seq_puts(s, |
| "Couldn't attach new domain to device. Is it already attached?\n"); |
| goto out_domain_free; |
| } |
| |
| seq_printf(s, "(average over %d iterations)\n", iters_per_op); |
| seq_printf(s, "%8s %19s %16s\n", "size", "iommu_map", "iommu_unmap"); |
| for (sz = sizes; *sz; ++sz) { |
| unsigned long size = *sz; |
| size_t unmapped; |
| u64 map_elapsed_ns = 0, unmap_elapsed_ns = 0; |
| u64 map_elapsed_us = 0, unmap_elapsed_us = 0; |
| u32 map_elapsed_rem = 0, unmap_elapsed_rem = 0; |
| struct timespec tbefore, tafter, diff; |
| int i; |
| |
| for (i = 0; i < iters_per_op; ++i) { |
| getnstimeofday(&tbefore); |
| if (iommu_map(domain, iova, paddr, size, |
| IOMMU_READ | IOMMU_WRITE)) { |
| seq_puts(s, "Failed to map\n"); |
| continue; |
| } |
| getnstimeofday(&tafter); |
| diff = timespec_sub(tafter, tbefore); |
| map_elapsed_ns += timespec_to_ns(&diff); |
| |
| getnstimeofday(&tbefore); |
| unmapped = iommu_unmap(domain, iova, size); |
| if (unmapped != size) { |
| seq_printf(s, |
| "Only unmapped %zx instead of %zx\n", |
| unmapped, size); |
| continue; |
| } |
| getnstimeofday(&tafter); |
| diff = timespec_sub(tafter, tbefore); |
| unmap_elapsed_ns += timespec_to_ns(&diff); |
| } |
| |
| map_elapsed_ns /= iters_per_op; |
| unmap_elapsed_ns /= iters_per_op; |
| |
| map_elapsed_us = div_u64_rem(map_elapsed_ns, 1000, |
| &map_elapsed_rem); |
| unmap_elapsed_us = div_u64_rem(unmap_elapsed_ns, 1000, |
| &unmap_elapsed_rem); |
| |
| seq_printf(s, "%8s %12lld.%03d us %9lld.%03d us\n", |
| _size_to_string(size), |
| map_elapsed_us, map_elapsed_rem, |
| unmap_elapsed_us, unmap_elapsed_rem); |
| } |
| |
| seq_putc(s, '\n'); |
| seq_printf(s, "%8s %19s %16s\n", "size", "iommu_map_sg", "iommu_unmap"); |
| for (sz = sizes; *sz; ++sz) { |
| unsigned long size = *sz; |
| size_t unmapped; |
| u64 map_elapsed_ns = 0, unmap_elapsed_ns = 0; |
| u64 map_elapsed_us = 0, unmap_elapsed_us = 0; |
| u32 map_elapsed_rem = 0, unmap_elapsed_rem = 0; |
| struct timespec tbefore, tafter, diff; |
| struct sg_table table; |
| unsigned long chunk_size = SZ_4K; |
| int i; |
| |
| if (iommu_debug_build_phoney_sg_table(dev, &table, size, |
| chunk_size)) { |
| seq_puts(s, |
| "couldn't build phoney sg table! bailing...\n"); |
| goto out_detach; |
| } |
| |
| for (i = 0; i < iters_per_op; ++i) { |
| getnstimeofday(&tbefore); |
| if (iommu_map_sg(domain, iova, table.sgl, table.nents, |
| IOMMU_READ | IOMMU_WRITE) != size) { |
| seq_puts(s, "Failed to map_sg\n"); |
| goto next; |
| } |
| getnstimeofday(&tafter); |
| diff = timespec_sub(tafter, tbefore); |
| map_elapsed_ns += timespec_to_ns(&diff); |
| |
| getnstimeofday(&tbefore); |
| unmapped = iommu_unmap(domain, iova, size); |
| if (unmapped != size) { |
| seq_printf(s, |
| "Only unmapped %zx instead of %zx\n", |
| unmapped, size); |
| goto next; |
| } |
| getnstimeofday(&tafter); |
| diff = timespec_sub(tafter, tbefore); |
| unmap_elapsed_ns += timespec_to_ns(&diff); |
| } |
| |
| map_elapsed_ns /= iters_per_op; |
| unmap_elapsed_ns /= iters_per_op; |
| |
| map_elapsed_us = div_u64_rem(map_elapsed_ns, 1000, |
| &map_elapsed_rem); |
| unmap_elapsed_us = div_u64_rem(unmap_elapsed_ns, 1000, |
| &unmap_elapsed_rem); |
| |
| seq_printf(s, "%8s %12lld.%03d us %9lld.%03d us\n", |
| _size_to_string(size), |
| map_elapsed_us, map_elapsed_rem, |
| unmap_elapsed_us, unmap_elapsed_rem); |
| |
| next: |
| iommu_debug_destroy_phoney_sg_table(dev, &table, chunk_size); |
| } |
| |
| out_detach: |
| iommu_detach_device(domain, dev); |
| out_domain_free: |
| iommu_domain_free(domain); |
| } |
| |
| static int iommu_debug_profiling_show(struct seq_file *s, void *ignored) |
| { |
| struct iommu_debug_device *ddev = s->private; |
| |
| iommu_debug_device_profiling(s, ddev->dev, false); |
| |
| return 0; |
| } |
| |
| static int iommu_debug_profiling_open(struct inode *inode, struct file *file) |
| { |
| return single_open(file, iommu_debug_profiling_show, inode->i_private); |
| } |
| |
| static const struct file_operations iommu_debug_profiling_fops = { |
| .open = iommu_debug_profiling_open, |
| .read = seq_read, |
| .llseek = seq_lseek, |
| .release = single_release, |
| }; |
| |
| static int iommu_debug_secure_profiling_show(struct seq_file *s, void *ignored) |
| { |
| struct iommu_debug_device *ddev = s->private; |
| |
| iommu_debug_device_profiling(s, ddev->dev, true); |
| |
| return 0; |
| } |
| |
| static int iommu_debug_secure_profiling_open(struct inode *inode, |
| struct file *file) |
| { |
| return single_open(file, iommu_debug_secure_profiling_show, |
| inode->i_private); |
| } |
| |
| static const struct file_operations iommu_debug_secure_profiling_fops = { |
| .open = iommu_debug_secure_profiling_open, |
| .read = seq_read, |
| .llseek = seq_lseek, |
| .release = single_release, |
| }; |
| |
| static int iommu_debug_attach_do_attach(struct iommu_debug_device *ddev, |
| int val, bool is_secure) |
| { |
| int htw_disable = 1; |
| struct bus_type *bus; |
| |
| bus = msm_iommu_get_bus(ddev->dev); |
| if (!bus) |
| return -EINVAL; |
| |
| ddev->domain = iommu_domain_alloc(bus); |
| if (!ddev->domain) { |
| pr_err("Couldn't allocate domain\n"); |
| return -ENOMEM; |
| } |
| |
| if (iommu_domain_set_attr(ddev->domain, |
| DOMAIN_ATTR_COHERENT_HTW_DISABLE, |
| &htw_disable)) { |
| pr_err("Couldn't disable coherent htw\n"); |
| goto out_domain_free; |
| } |
| |
| if (is_secure && iommu_domain_set_attr(ddev->domain, |
| DOMAIN_ATTR_SECURE_VMID, |
| &val)) { |
| pr_err("Couldn't set secure vmid to %d\n", val); |
| goto out_domain_free; |
| } |
| |
| if (iommu_attach_device(ddev->domain, ddev->dev)) { |
| pr_err("Couldn't attach new domain to device. Is it already attached?\n"); |
| goto out_domain_free; |
| } |
| |
| return 0; |
| |
| out_domain_free: |
| iommu_domain_free(ddev->domain); |
| ddev->domain = NULL; |
| return -EIO; |
| } |
| |
| static ssize_t __iommu_debug_attach_write(struct file *file, |
| const char __user *ubuf, |
| size_t count, loff_t *offset, |
| bool is_secure) |
| { |
| struct iommu_debug_device *ddev = file->private_data; |
| ssize_t retval; |
| int val; |
| |
| if (kstrtoint_from_user(ubuf, count, 0, &val)) { |
| pr_err("Invalid format. Expected a hex or decimal integer"); |
| retval = -EFAULT; |
| goto out; |
| } |
| |
| if (val) { |
| if (ddev->domain) { |
| pr_err("Already attached.\n"); |
| retval = -EINVAL; |
| goto out; |
| } |
| if (WARN(ddev->dev->archdata.iommu, |
| "Attachment tracking out of sync with device\n")) { |
| retval = -EINVAL; |
| goto out; |
| } |
| if (iommu_debug_attach_do_attach(ddev, val, is_secure)) { |
| retval = -EIO; |
| goto out; |
| } |
| pr_err("Attached\n"); |
| } else { |
| if (!ddev->domain) { |
| pr_err("No domain. Did you already attach?\n"); |
| retval = -EINVAL; |
| goto out; |
| } |
| iommu_detach_device(ddev->domain, ddev->dev); |
| iommu_domain_free(ddev->domain); |
| ddev->domain = NULL; |
| pr_err("Detached\n"); |
| } |
| |
| retval = count; |
| out: |
| return retval; |
| } |
| |
| static ssize_t iommu_debug_attach_write(struct file *file, |
| const char __user *ubuf, |
| size_t count, loff_t *offset) |
| { |
| return __iommu_debug_attach_write(file, ubuf, count, offset, |
| false); |
| |
| } |
| |
| static ssize_t iommu_debug_attach_read(struct file *file, char __user *ubuf, |
| size_t count, loff_t *offset) |
| { |
| struct iommu_debug_device *ddev = file->private_data; |
| char c[2]; |
| |
| if (*offset) |
| return 0; |
| |
| c[0] = ddev->domain ? '1' : '0'; |
| c[1] = '\n'; |
| if (copy_to_user(ubuf, &c, 2)) { |
| pr_err("copy_to_user failed\n"); |
| return -EFAULT; |
| } |
| *offset = 1; /* non-zero means we're done */ |
| |
| return 2; |
| } |
| |
| static const struct file_operations iommu_debug_attach_fops = { |
| .open = simple_open, |
| .write = iommu_debug_attach_write, |
| .read = iommu_debug_attach_read, |
| }; |
| |
| static ssize_t iommu_debug_attach_write_secure(struct file *file, |
| const char __user *ubuf, |
| size_t count, loff_t *offset) |
| { |
| return __iommu_debug_attach_write(file, ubuf, count, offset, |
| true); |
| |
| } |
| |
| static const struct file_operations iommu_debug_secure_attach_fops = { |
| .open = simple_open, |
| .write = iommu_debug_attach_write_secure, |
| .read = iommu_debug_attach_read, |
| }; |
| |
| static ssize_t iommu_debug_atos_write(struct file *file, |
| const char __user *ubuf, |
| size_t count, loff_t *offset) |
| { |
| struct iommu_debug_device *ddev = file->private_data; |
| dma_addr_t iova; |
| |
| if (kstrtoll_from_user(ubuf, count, 0, &iova)) { |
| pr_err("Invalid format for iova\n"); |
| ddev->iova = 0; |
| return -EINVAL; |
| } |
| |
| ddev->iova = iova; |
| pr_err("Saved iova=%pa for future ATOS commands\n", &iova); |
| return count; |
| } |
| |
| static ssize_t iommu_debug_atos_read(struct file *file, char __user *ubuf, |
| size_t count, loff_t *offset) |
| { |
| struct iommu_debug_device *ddev = file->private_data; |
| phys_addr_t phys; |
| char buf[100]; |
| ssize_t retval; |
| size_t buflen; |
| |
| if (!ddev->domain) { |
| pr_err("No domain. Did you already attach?\n"); |
| return -EINVAL; |
| } |
| |
| if (*offset) |
| return 0; |
| |
| memset(buf, 0, 100); |
| |
| phys = iommu_iova_to_phys_hard(ddev->domain, ddev->iova); |
| if (!phys) |
| strlcpy(buf, "FAIL\n", 100); |
| else |
| snprintf(buf, 100, "%pa\n", &phys); |
| |
| buflen = strlen(buf); |
| if (copy_to_user(ubuf, buf, buflen)) { |
| pr_err("Couldn't copy_to_user\n"); |
| retval = -EFAULT; |
| } else { |
| *offset = 1; /* non-zero means we're done */ |
| retval = buflen; |
| } |
| |
| return retval; |
| } |
| |
| static const struct file_operations iommu_debug_atos_fops = { |
| .open = simple_open, |
| .write = iommu_debug_atos_write, |
| .read = iommu_debug_atos_read, |
| }; |
| |
| static ssize_t iommu_debug_map_write(struct file *file, const char __user *ubuf, |
| size_t count, loff_t *offset) |
| { |
| ssize_t retval; |
| int ret; |
| char *comma1, *comma2, *comma3; |
| char buf[100]; |
| dma_addr_t iova; |
| phys_addr_t phys; |
| size_t size; |
| int prot; |
| struct iommu_debug_device *ddev = file->private_data; |
| |
| if (count >= 100) { |
| pr_err("Value too large\n"); |
| return -EINVAL; |
| } |
| |
| if (!ddev->domain) { |
| pr_err("No domain. Did you already attach?\n"); |
| return -EINVAL; |
| } |
| |
| memset(buf, 0, 100); |
| |
| if (copy_from_user(buf, ubuf, count)) { |
| pr_err("Couldn't copy from user\n"); |
| retval = -EFAULT; |
| } |
| |
| comma1 = strnchr(buf, count, ','); |
| if (!comma1) |
| goto invalid_format; |
| |
| comma2 = strnchr(comma1 + 1, count, ','); |
| if (!comma2) |
| goto invalid_format; |
| |
| comma3 = strnchr(comma2 + 1, count, ','); |
| if (!comma3) |
| goto invalid_format; |
| |
| /* split up the words */ |
| *comma1 = *comma2 = *comma3 = '\0'; |
| |
| if (kstrtou64(buf, 0, &iova)) |
| goto invalid_format; |
| |
| if (kstrtou64(comma1 + 1, 0, &phys)) |
| goto invalid_format; |
| |
| if (kstrtoul(comma2 + 1, 0, &size)) |
| goto invalid_format; |
| |
| if (kstrtoint(comma3 + 1, 0, &prot)) |
| goto invalid_format; |
| |
| ret = iommu_map(ddev->domain, iova, phys, size, prot); |
| if (ret) { |
| pr_err("iommu_map failed with %d\n", ret); |
| retval = -EIO; |
| goto out; |
| } |
| |
| retval = count; |
| pr_err("Mapped %pa to %pa (len=0x%zx, prot=0x%x)\n", |
| &iova, &phys, size, prot); |
| out: |
| return retval; |
| |
| invalid_format: |
| pr_err("Invalid format. Expected: iova,phys,len,prot where `prot' is the bitwise OR of IOMMU_READ, IOMMU_WRITE, etc.\n"); |
| return retval; |
| } |
| |
| static const struct file_operations iommu_debug_map_fops = { |
| .open = simple_open, |
| .write = iommu_debug_map_write, |
| }; |
| |
| static ssize_t iommu_debug_unmap_write(struct file *file, |
| const char __user *ubuf, |
| size_t count, loff_t *offset) |
| { |
| ssize_t retval; |
| char *comma1; |
| char buf[100]; |
| dma_addr_t iova; |
| size_t size; |
| size_t unmapped; |
| struct iommu_debug_device *ddev = file->private_data; |
| |
| if (count >= 100) { |
| pr_err("Value too large\n"); |
| return -EINVAL; |
| } |
| |
| if (!ddev->domain) { |
| pr_err("No domain. Did you already attach?\n"); |
| return -EINVAL; |
| } |
| |
| memset(buf, 0, 100); |
| |
| if (copy_from_user(buf, ubuf, count)) { |
| pr_err("Couldn't copy from user\n"); |
| retval = -EFAULT; |
| goto out; |
| } |
| |
| comma1 = strnchr(buf, count, ','); |
| if (!comma1) |
| goto invalid_format; |
| |
| /* split up the words */ |
| *comma1 = '\0'; |
| |
| if (kstrtou64(buf, 0, &iova)) |
| goto invalid_format; |
| |
| if (kstrtoul(comma1 + 1, 0, &size)) |
| goto invalid_format; |
| |
| unmapped = iommu_unmap(ddev->domain, iova, size); |
| if (unmapped != size) { |
| pr_err("iommu_unmap failed. Expected to unmap: 0x%zx, unmapped: 0x%zx", |
| size, unmapped); |
| return -EIO; |
| } |
| |
| retval = count; |
| pr_err("Unmapped %pa (len=0x%zx)\n", &iova, size); |
| out: |
| return retval; |
| |
| invalid_format: |
| pr_err("Invalid format. Expected: iova,len\n"); |
| return retval; |
| } |
| |
| static const struct file_operations iommu_debug_unmap_fops = { |
| .open = simple_open, |
| .write = iommu_debug_unmap_write, |
| }; |
| |
| /* |
| * The following will only work for drivers that implement the generic |
| * device tree bindings described in |
| * Documentation/devicetree/bindings/iommu/iommu.txt |
| */ |
| static int snarf_iommu_devices(struct device *dev, const char *name) |
| { |
| struct iommu_debug_device *ddev; |
| struct dentry *dir; |
| |
| if (IS_ERR_OR_NULL(dev)) |
| return -EINVAL; |
| |
| ddev = kzalloc(sizeof(*ddev), GFP_KERNEL); |
| if (!ddev) |
| return -ENODEV; |
| ddev->dev = dev; |
| dir = debugfs_create_dir(name, debugfs_tests_dir); |
| if (!dir) { |
| pr_err("Couldn't create iommu/devices/%s debugfs dir\n", |
| name); |
| goto err; |
| } |
| |
| if (!debugfs_create_file("nr_iters", S_IRUSR, dir, &iters_per_op, |
| &iommu_debug_nr_iters_ops)) { |
| pr_err("Couldn't create iommu/devices/%s/nr_iters debugfs file\n", |
| name); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file("profiling", S_IRUSR, dir, ddev, |
| &iommu_debug_profiling_fops)) { |
| pr_err("Couldn't create iommu/devices/%s/profiling debugfs file\n", |
| name); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file("secure_profiling", S_IRUSR, dir, ddev, |
| &iommu_debug_secure_profiling_fops)) { |
| pr_err("Couldn't create iommu/devices/%s/secure_profiling debugfs file\n", |
| name); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file("attach", S_IRUSR, dir, ddev, |
| &iommu_debug_attach_fops)) { |
| pr_err("Couldn't create iommu/devices/%s/attach debugfs file\n", |
| name); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file("secure_attach", S_IRUSR, dir, ddev, |
| &iommu_debug_secure_attach_fops)) { |
| pr_err("Couldn't create iommu/devices/%s/secure_attach debugfs file\n", |
| name); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file("atos", S_IWUSR, dir, ddev, |
| &iommu_debug_atos_fops)) { |
| pr_err("Couldn't create iommu/devices/%s/atos debugfs file\n", |
| name); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file("map", S_IWUSR, dir, ddev, |
| &iommu_debug_map_fops)) { |
| pr_err("Couldn't create iommu/devices/%s/map debugfs file\n", |
| name); |
| goto err_rmdir; |
| } |
| |
| if (!debugfs_create_file("unmap", S_IWUSR, dir, ddev, |
| &iommu_debug_unmap_fops)) { |
| pr_err("Couldn't create iommu/devices/%s/unmap debugfs file\n", |
| name); |
| goto err_rmdir; |
| } |
| |
| list_add(&ddev->list, &iommu_debug_devices); |
| return 0; |
| |
| err_rmdir: |
| debugfs_remove_recursive(dir); |
| err: |
| kfree(ddev); |
| return 0; |
| } |
| |
| static int pass_iommu_devices(struct device *dev, void *ignored) |
| { |
| if (!of_find_property(dev->of_node, "iommus", NULL)) |
| return 0; |
| |
| return snarf_iommu_devices(dev, dev_name(dev)); |
| } |
| |
| static int iommu_debug_populate_devices(void) |
| { |
| int ret; |
| struct device_node *np; |
| const char *cb_name; |
| |
| for_each_compatible_node(np, NULL, "qcom,msm-smmu-v2-ctx") { |
| ret = of_property_read_string(np, "label", &cb_name); |
| if (ret) |
| return ret; |
| |
| ret = snarf_iommu_devices(msm_iommu_get_ctx(cb_name), cb_name); |
| if (ret) |
| return ret; |
| } |
| |
| return bus_for_each_dev(&platform_bus_type, NULL, NULL, |
| pass_iommu_devices); |
| } |
| |
| static int iommu_debug_init_tests(void) |
| { |
| debugfs_tests_dir = debugfs_create_dir("tests", |
| iommu_debugfs_top); |
| if (!debugfs_tests_dir) { |
| pr_err("Couldn't create iommu/tests debugfs directory\n"); |
| return -ENODEV; |
| } |
| |
| return iommu_debug_populate_devices(); |
| } |
| |
| static void iommu_debug_destroy_tests(void) |
| { |
| debugfs_remove_recursive(debugfs_tests_dir); |
| } |
| #else |
| static inline int iommu_debug_init_tests(void) { return 0; } |
| static inline void iommu_debug_destroy_tests(void) { } |
| #endif |
| |
| static int iommu_debug_init(void) |
| { |
| if (iommu_debug_init_tracking()) |
| return -ENODEV; |
| |
| if (iommu_debug_init_tests()) |
| return -ENODEV; |
| |
| return 0; |
| } |
| |
| static void iommu_debug_exit(void) |
| { |
| iommu_debug_destroy_tracking(); |
| iommu_debug_destroy_tests(); |
| } |
| |
| module_init(iommu_debug_init); |
| module_exit(iommu_debug_exit); |