| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * AMD IOMMU driver | |
| * | |
| * Copyright (C) 2018 Advanced Micro Devices, Inc. | |
| * | |
| * Author: Gary R Hook <gary.hook@amd.com> | |
| */ | |
| #include <linux/debugfs.h> | |
| #include <linux/pci.h> | |
| #include "amd_iommu.h" | |
| #include "../irq_remapping.h" | |
| static struct dentry *amd_iommu_debugfs; | |
| #define MAX_NAME_LEN 20 | |
| #define OFS_IN_SZ 8 | |
| #define DEVID_IN_SZ 16 | |
| static int sbdf = -1; | |
| static ssize_t iommu_mmio_write(struct file *filp, const char __user *ubuf, | |
| size_t cnt, loff_t *ppos) | |
| { | |
| struct seq_file *m = filp->private_data; | |
| struct amd_iommu *iommu = m->private; | |
| int ret, dbg_mmio_offset = iommu->dbg_mmio_offset = -1; | |
| if (cnt > OFS_IN_SZ) | |
| return -EINVAL; | |
| ret = kstrtos32_from_user(ubuf, cnt, 0, &dbg_mmio_offset); | |
| if (ret) | |
| return ret; | |
| if (dbg_mmio_offset < 0 || dbg_mmio_offset > | |
| iommu->mmio_phys_end - sizeof(u64)) | |
| return -EINVAL; | |
| iommu->dbg_mmio_offset = dbg_mmio_offset; | |
| return cnt; | |
| } | |
| static int iommu_mmio_show(struct seq_file *m, void *unused) | |
| { | |
| struct amd_iommu *iommu = m->private; | |
| u64 value; | |
| int dbg_mmio_offset = iommu->dbg_mmio_offset; | |
| if (dbg_mmio_offset < 0 || dbg_mmio_offset > | |
| iommu->mmio_phys_end - sizeof(u64)) { | |
| seq_puts(m, "Please provide mmio register's offset\n"); | |
| return 0; | |
| } | |
| value = readq(iommu->mmio_base + dbg_mmio_offset); | |
| seq_printf(m, "Offset:0x%x Value:0x%016llx\n", dbg_mmio_offset, value); | |
| return 0; | |
| } | |
| DEFINE_SHOW_STORE_ATTRIBUTE(iommu_mmio); | |
| static ssize_t iommu_capability_write(struct file *filp, const char __user *ubuf, | |
| size_t cnt, loff_t *ppos) | |
| { | |
| struct seq_file *m = filp->private_data; | |
| struct amd_iommu *iommu = m->private; | |
| int ret, dbg_cap_offset = iommu->dbg_cap_offset = -1; | |
| if (cnt > OFS_IN_SZ) | |
| return -EINVAL; | |
| ret = kstrtos32_from_user(ubuf, cnt, 0, &dbg_cap_offset); | |
| if (ret) | |
| return ret; | |
| /* Capability register at offset 0x14 is the last IOMMU capability register. */ | |
| if (dbg_cap_offset < 0 || dbg_cap_offset > 0x14) | |
| return -EINVAL; | |
| iommu->dbg_cap_offset = dbg_cap_offset; | |
| return cnt; | |
| } | |
| static int iommu_capability_show(struct seq_file *m, void *unused) | |
| { | |
| struct amd_iommu *iommu = m->private; | |
| u32 value; | |
| int err, dbg_cap_offset = iommu->dbg_cap_offset; | |
| if (dbg_cap_offset < 0 || dbg_cap_offset > 0x14) { | |
| seq_puts(m, "Please provide capability register's offset in the range [0x00 - 0x14]\n"); | |
| return 0; | |
| } | |
| err = pci_read_config_dword(iommu->dev, iommu->cap_ptr + dbg_cap_offset, &value); | |
| if (err) { | |
| seq_printf(m, "Not able to read capability register at 0x%x\n", | |
| dbg_cap_offset); | |
| return 0; | |
| } | |
| seq_printf(m, "Offset:0x%x Value:0x%08x\n", dbg_cap_offset, value); | |
| return 0; | |
| } | |
| DEFINE_SHOW_STORE_ATTRIBUTE(iommu_capability); | |
| static int iommu_cmdbuf_show(struct seq_file *m, void *unused) | |
| { | |
| struct amd_iommu *iommu = m->private; | |
| struct iommu_cmd *cmd; | |
| unsigned long flag; | |
| u32 head, tail; | |
| int i; | |
| raw_spin_lock_irqsave(&iommu->lock, flag); | |
| head = readl(iommu->mmio_base + MMIO_CMD_HEAD_OFFSET); | |
| tail = readl(iommu->mmio_base + MMIO_CMD_TAIL_OFFSET); | |
| seq_printf(m, "CMD Buffer Head Offset:%d Tail Offset:%d\n", | |
| (head >> 4) & 0x7fff, (tail >> 4) & 0x7fff); | |
| for (i = 0; i < CMD_BUFFER_ENTRIES; i++) { | |
| cmd = (struct iommu_cmd *)(iommu->cmd_buf + i * sizeof(*cmd)); | |
| seq_printf(m, "%3d: %08x %08x %08x %08x\n", i, cmd->data[0], | |
| cmd->data[1], cmd->data[2], cmd->data[3]); | |
| } | |
| raw_spin_unlock_irqrestore(&iommu->lock, flag); | |
| return 0; | |
| } | |
| DEFINE_SHOW_ATTRIBUTE(iommu_cmdbuf); | |
| static ssize_t devid_write(struct file *filp, const char __user *ubuf, | |
| size_t cnt, loff_t *ppos) | |
| { | |
| struct amd_iommu_pci_seg *pci_seg; | |
| int seg, bus, slot, func; | |
| struct amd_iommu *iommu; | |
| char *srcid_ptr; | |
| u16 devid; | |
| int i; | |
| sbdf = -1; | |
| if (cnt >= DEVID_IN_SZ) | |
| return -EINVAL; | |
| srcid_ptr = memdup_user_nul(ubuf, cnt); | |
| if (IS_ERR(srcid_ptr)) | |
| return PTR_ERR(srcid_ptr); | |
| i = sscanf(srcid_ptr, "%x:%x:%x.%x", &seg, &bus, &slot, &func); | |
| if (i != 4) { | |
| i = sscanf(srcid_ptr, "%x:%x.%x", &bus, &slot, &func); | |
| if (i != 3) { | |
| kfree(srcid_ptr); | |
| return -EINVAL; | |
| } | |
| seg = 0; | |
| } | |
| devid = PCI_DEVID(bus, PCI_DEVFN(slot, func)); | |
| /* Check if user device id input is a valid input */ | |
| for_each_pci_segment(pci_seg) { | |
| if (pci_seg->id != seg) | |
| continue; | |
| if (devid > pci_seg->last_bdf) { | |
| kfree(srcid_ptr); | |
| return -EINVAL; | |
| } | |
| iommu = pci_seg->rlookup_table[devid]; | |
| if (!iommu) { | |
| kfree(srcid_ptr); | |
| return -ENODEV; | |
| } | |
| sbdf = PCI_SEG_DEVID_TO_SBDF(seg, devid); | |
| kfree(srcid_ptr); | |
| return cnt; | |
| } | |
| kfree(srcid_ptr); | |
| return -EINVAL; | |
| } | |
| static int devid_show(struct seq_file *m, void *unused) | |
| { | |
| u16 devid; | |
| int sbdf_shadow = sbdf; | |
| if (sbdf_shadow >= 0) { | |
| devid = PCI_SBDF_TO_DEVID(sbdf_shadow); | |
| seq_printf(m, "%04x:%02x:%02x.%x\n", PCI_SBDF_TO_SEGID(sbdf_shadow), | |
| PCI_BUS_NUM(devid), PCI_SLOT(devid), PCI_FUNC(devid)); | |
| } else | |
| seq_puts(m, "No or Invalid input provided\n"); | |
| return 0; | |
| } | |
| DEFINE_SHOW_STORE_ATTRIBUTE(devid); | |
| static void dump_dte(struct seq_file *m, struct amd_iommu_pci_seg *pci_seg, u16 devid) | |
| { | |
| struct dev_table_entry *dev_table; | |
| struct amd_iommu *iommu; | |
| iommu = pci_seg->rlookup_table[devid]; | |
| if (!iommu) | |
| return; | |
| dev_table = get_dev_table(iommu); | |
| if (!dev_table) { | |
| seq_puts(m, "Device table not found"); | |
| return; | |
| } | |
| seq_printf(m, "%-12s %16s %16s %16s %16s iommu\n", "DeviceId", | |
| "QWORD[3]", "QWORD[2]", "QWORD[1]", "QWORD[0]"); | |
| seq_printf(m, "%04x:%02x:%02x.%x ", pci_seg->id, PCI_BUS_NUM(devid), | |
| PCI_SLOT(devid), PCI_FUNC(devid)); | |
| for (int i = 3; i >= 0; --i) | |
| seq_printf(m, "%016llx ", dev_table[devid].data[i]); | |
| seq_printf(m, "iommu%d\n", iommu->index); | |
| } | |
| static int iommu_devtbl_show(struct seq_file *m, void *unused) | |
| { | |
| struct amd_iommu_pci_seg *pci_seg; | |
| u16 seg, devid; | |
| int sbdf_shadow = sbdf; | |
| if (sbdf_shadow < 0) { | |
| seq_puts(m, "Enter a valid device ID to 'devid' file\n"); | |
| return 0; | |
| } | |
| seg = PCI_SBDF_TO_SEGID(sbdf_shadow); | |
| devid = PCI_SBDF_TO_DEVID(sbdf_shadow); | |
| for_each_pci_segment(pci_seg) { | |
| if (pci_seg->id != seg) | |
| continue; | |
| dump_dte(m, pci_seg, devid); | |
| break; | |
| } | |
| return 0; | |
| } | |
| DEFINE_SHOW_ATTRIBUTE(iommu_devtbl); | |
| static void dump_128_irte(struct seq_file *m, struct irq_remap_table *table, u16 int_tab_len) | |
| { | |
| struct irte_ga *ptr, *irte; | |
| int index; | |
| for (index = 0; index < int_tab_len; index++) { | |
| ptr = (struct irte_ga *)table->table; | |
| irte = &ptr[index]; | |
| if (AMD_IOMMU_GUEST_IR_VAPIC(amd_iommu_guest_ir) && | |
| !irte->lo.fields_vapic.valid) | |
| continue; | |
| else if (!irte->lo.fields_remap.valid) | |
| continue; | |
| seq_printf(m, "IRT[%04d] %016llx %016llx\n", index, irte->hi.val, irte->lo.val); | |
| } | |
| } | |
| static void dump_32_irte(struct seq_file *m, struct irq_remap_table *table, u16 int_tab_len) | |
| { | |
| union irte *ptr, *irte; | |
| int index; | |
| for (index = 0; index < int_tab_len; index++) { | |
| ptr = (union irte *)table->table; | |
| irte = &ptr[index]; | |
| if (!irte->fields.valid) | |
| continue; | |
| seq_printf(m, "IRT[%04d] %08x\n", index, irte->val); | |
| } | |
| } | |
| static void dump_irte(struct seq_file *m, u16 devid, struct amd_iommu_pci_seg *pci_seg) | |
| { | |
| struct dev_table_entry *dev_table; | |
| struct irq_remap_table *table; | |
| struct amd_iommu *iommu; | |
| unsigned long flags; | |
| u16 int_tab_len; | |
| table = pci_seg->irq_lookup_table[devid]; | |
| if (!table) { | |
| seq_printf(m, "IRQ lookup table not set for %04x:%02x:%02x:%x\n", | |
| pci_seg->id, PCI_BUS_NUM(devid), PCI_SLOT(devid), PCI_FUNC(devid)); | |
| return; | |
| } | |
| iommu = pci_seg->rlookup_table[devid]; | |
| if (!iommu) | |
| return; | |
| dev_table = get_dev_table(iommu); | |
| if (!dev_table) { | |
| seq_puts(m, "Device table not found"); | |
| return; | |
| } | |
| int_tab_len = dev_table[devid].data[2] & DTE_INTTABLEN_MASK; | |
| if (int_tab_len != DTE_INTTABLEN_512 && int_tab_len != DTE_INTTABLEN_2K) { | |
| seq_puts(m, "The device's DTE contains an invalid IRT length value."); | |
| return; | |
| } | |
| seq_printf(m, "DeviceId %04x:%02x:%02x.%x\n", pci_seg->id, PCI_BUS_NUM(devid), | |
| PCI_SLOT(devid), PCI_FUNC(devid)); | |
| raw_spin_lock_irqsave(&table->lock, flags); | |
| if (AMD_IOMMU_GUEST_IR_GA(amd_iommu_guest_ir)) | |
| dump_128_irte(m, table, BIT(int_tab_len >> 1)); | |
| else | |
| dump_32_irte(m, table, BIT(int_tab_len >> 1)); | |
| seq_puts(m, "\n"); | |
| raw_spin_unlock_irqrestore(&table->lock, flags); | |
| } | |
| static int iommu_irqtbl_show(struct seq_file *m, void *unused) | |
| { | |
| struct amd_iommu_pci_seg *pci_seg; | |
| u16 devid, seg; | |
| int sbdf_shadow = sbdf; | |
| if (!irq_remapping_enabled) { | |
| seq_puts(m, "Interrupt remapping is disabled\n"); | |
| return 0; | |
| } | |
| if (sbdf_shadow < 0) { | |
| seq_puts(m, "Enter a valid device ID to 'devid' file\n"); | |
| return 0; | |
| } | |
| seg = PCI_SBDF_TO_SEGID(sbdf_shadow); | |
| devid = PCI_SBDF_TO_DEVID(sbdf_shadow); | |
| for_each_pci_segment(pci_seg) { | |
| if (pci_seg->id != seg) | |
| continue; | |
| dump_irte(m, devid, pci_seg); | |
| break; | |
| } | |
| return 0; | |
| } | |
| DEFINE_SHOW_ATTRIBUTE(iommu_irqtbl); | |
| void amd_iommu_debugfs_setup(void) | |
| { | |
| struct amd_iommu *iommu; | |
| char name[MAX_NAME_LEN + 1]; | |
| amd_iommu_debugfs = debugfs_create_dir("amd", iommu_debugfs_dir); | |
| for_each_iommu(iommu) { | |
| iommu->dbg_mmio_offset = -1; | |
| iommu->dbg_cap_offset = -1; | |
| snprintf(name, MAX_NAME_LEN, "iommu%02d", iommu->index); | |
| iommu->debugfs = debugfs_create_dir(name, amd_iommu_debugfs); | |
| debugfs_create_file("mmio", 0644, iommu->debugfs, iommu, | |
| &iommu_mmio_fops); | |
| debugfs_create_file("capability", 0644, iommu->debugfs, iommu, | |
| &iommu_capability_fops); | |
| debugfs_create_file("cmdbuf", 0444, iommu->debugfs, iommu, | |
| &iommu_cmdbuf_fops); | |
| } | |
| debugfs_create_file("devid", 0644, amd_iommu_debugfs, NULL, | |
| &devid_fops); | |
| debugfs_create_file("devtbl", 0444, amd_iommu_debugfs, NULL, | |
| &iommu_devtbl_fops); | |
| debugfs_create_file("irqtbl", 0444, amd_iommu_debugfs, NULL, | |
| &iommu_irqtbl_fops); | |
| } |