/* * Generic functionality for handling accesses to the PCI configuration space * from guests. * * Copyright (C) 2017 Citrix Systems R&D * * This program is free software; you can redistribute it and/or * modify it under the terms and conditions of the GNU General Public * License, 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. * * You should have received a copy of the GNU General Public * License along with this program; If not, see . */ #include #include /* Internal struct to store the emulated PCI registers. */ struct vpci_register { vpci_read_t *read; vpci_write_t *write; unsigned int size; unsigned int offset; void *private; struct list_head node; }; #ifdef __XEN__ extern vpci_register_init_t *const __start_vpci_array[]; extern vpci_register_init_t *const __end_vpci_array[]; #define NUM_VPCI_INIT (__end_vpci_array - __start_vpci_array) void vpci_remove_device(struct pci_dev *pdev) { spin_lock(&pdev->vpci->lock); while ( !list_empty(&pdev->vpci->handlers) ) { struct vpci_register *r = list_first_entry(&pdev->vpci->handlers, struct vpci_register, node); list_del(&r->node); xfree(r); } spin_unlock(&pdev->vpci->lock); xfree(pdev->vpci->msix); xfree(pdev->vpci->msi); xfree(pdev->vpci); pdev->vpci = NULL; } int __hwdom_init vpci_add_handlers(struct pci_dev *pdev) { unsigned int i; int rc = 0; if ( !has_vpci(pdev->domain) ) return 0; pdev->vpci = xzalloc(struct vpci); if ( !pdev->vpci ) return -ENOMEM; INIT_LIST_HEAD(&pdev->vpci->handlers); spin_lock_init(&pdev->vpci->lock); for ( i = 0; i < NUM_VPCI_INIT; i++ ) { rc = __start_vpci_array[i](pdev); if ( rc ) break; } if ( rc ) vpci_remove_device(pdev); return rc; } #endif /* __XEN__ */ static int vpci_register_cmp(const struct vpci_register *r1, const struct vpci_register *r2) { /* Return 0 if registers overlap. */ if ( r1->offset < r2->offset + r2->size && r2->offset < r1->offset + r1->size ) return 0; if ( r1->offset < r2->offset ) return -1; if ( r1->offset > r2->offset ) return 1; ASSERT_UNREACHABLE(); return 0; } /* Dummy hooks, writes are ignored, reads return 1's */ static uint32_t vpci_ignored_read(const struct pci_dev *pdev, unsigned int reg, void *data) { return ~(uint32_t)0; } static void vpci_ignored_write(const struct pci_dev *pdev, unsigned int reg, uint32_t val, void *data) { } uint32_t vpci_hw_read16(const struct pci_dev *pdev, unsigned int reg, void *data) { return pci_conf_read16(pdev->sbdf, reg); } uint32_t vpci_hw_read32(const struct pci_dev *pdev, unsigned int reg, void *data) { return pci_conf_read32(pdev->sbdf, reg); } int vpci_add_register(struct vpci *vpci, vpci_read_t *read_handler, vpci_write_t *write_handler, unsigned int offset, unsigned int size, void *data) { struct list_head *prev; struct vpci_register *r; /* Some sanity checks. */ if ( (size != 1 && size != 2 && size != 4) || offset >= PCI_CFG_SPACE_EXP_SIZE || (offset & (size - 1)) || (!read_handler && !write_handler) ) return -EINVAL; r = xmalloc(struct vpci_register); if ( !r ) return -ENOMEM; r->read = read_handler ?: vpci_ignored_read; r->write = write_handler ?: vpci_ignored_write; r->size = size; r->offset = offset; r->private = data; spin_lock(&vpci->lock); /* The list of handlers must be kept sorted at all times. */ list_for_each ( prev, &vpci->handlers ) { const struct vpci_register *this = list_entry(prev, const struct vpci_register, node); int cmp = vpci_register_cmp(r, this); if ( cmp < 0 ) break; if ( cmp == 0 ) { spin_unlock(&vpci->lock); xfree(r); return -EEXIST; } } list_add_tail(&r->node, prev); spin_unlock(&vpci->lock); return 0; } int vpci_remove_register(struct vpci *vpci, unsigned int offset, unsigned int size) { const struct vpci_register r = { .offset = offset, .size = size }; struct vpci_register *rm; spin_lock(&vpci->lock); list_for_each_entry ( rm, &vpci->handlers, node ) { int cmp = vpci_register_cmp(&r, rm); /* * NB: do not use a switch so that we can use break to * get out of the list loop earlier if required. */ if ( !cmp && rm->offset == offset && rm->size == size ) { list_del(&rm->node); spin_unlock(&vpci->lock); xfree(rm); return 0; } if ( cmp <= 0 ) break; } spin_unlock(&vpci->lock); return -ENOENT; } /* Wrappers for performing reads/writes to the underlying hardware. */ static uint32_t vpci_read_hw(pci_sbdf_t sbdf, unsigned int reg, unsigned int size) { uint32_t data; switch ( size ) { case 4: data = pci_conf_read32(sbdf, reg); break; case 3: /* * This is possible because a 4byte read can have 1byte trapped and * the rest passed-through. */ if ( reg & 1 ) { data = pci_conf_read8(sbdf, reg); data |= pci_conf_read16(sbdf, reg + 1) << 8; } else { data = pci_conf_read16(sbdf, reg); data |= pci_conf_read8(sbdf, reg + 2) << 16; } break; case 2: data = pci_conf_read16(sbdf, reg); break; case 1: data = pci_conf_read8(sbdf, reg); break; default: ASSERT_UNREACHABLE(); data = ~(uint32_t)0; break; } return data; } static void vpci_write_hw(pci_sbdf_t sbdf, unsigned int reg, unsigned int size, uint32_t data) { switch ( size ) { case 4: pci_conf_write32(sbdf, reg, data); break; case 3: /* * This is possible because a 4byte write can have 1byte trapped and * the rest passed-through. */ if ( reg & 1 ) { pci_conf_write8(sbdf, reg, data); pci_conf_write16(sbdf, reg + 1, data >> 8); } else { pci_conf_write16(sbdf, reg, data); pci_conf_write8(sbdf, reg + 2, data >> 16); } break; case 2: pci_conf_write16(sbdf, reg, data); break; case 1: pci_conf_write8(sbdf, reg, data); break; default: ASSERT_UNREACHABLE(); break; } } /* * Merge new data into a partial result. * * Copy the value found in 'new' from [0, size) left shifted by * 'offset' into 'data'. Note that both 'size' and 'offset' are * in byte units. */ static uint32_t merge_result(uint32_t data, uint32_t new, unsigned int size, unsigned int offset) { uint32_t mask = 0xffffffff >> (32 - 8 * size); return (data & ~(mask << (offset * 8))) | ((new & mask) << (offset * 8)); } uint32_t vpci_read(pci_sbdf_t sbdf, unsigned int reg, unsigned int size) { const struct domain *d = current->domain; const struct pci_dev *pdev; const struct vpci_register *r; unsigned int data_offset = 0; uint32_t data = ~(uint32_t)0; if ( !size ) { ASSERT_UNREACHABLE(); return data; } /* Find the PCI dev matching the address. */ pdev = pci_get_pdev_by_domain(d, sbdf.seg, sbdf.bus, sbdf.devfn); if ( !pdev ) return vpci_read_hw(sbdf, reg, size); spin_lock(&pdev->vpci->lock); /* Read from the hardware or the emulated register handlers. */ list_for_each_entry ( r, &pdev->vpci->handlers, node ) { const struct vpci_register emu = { .offset = reg + data_offset, .size = size - data_offset }; int cmp = vpci_register_cmp(&emu, r); uint32_t val; unsigned int read_size; if ( cmp < 0 ) break; if ( cmp > 0 ) continue; if ( emu.offset < r->offset ) { /* Heading gap, read partial content from hardware. */ read_size = r->offset - emu.offset; val = vpci_read_hw(sbdf, emu.offset, read_size); data = merge_result(data, val, read_size, data_offset); data_offset += read_size; } val = r->read(pdev, r->offset, r->private); /* Check if the read is in the middle of a register. */ if ( r->offset < emu.offset ) val >>= (emu.offset - r->offset) * 8; /* Find the intersection size between the two sets. */ read_size = min(emu.offset + emu.size, r->offset + r->size) - max(emu.offset, r->offset); /* Merge the emulated data into the native read value. */ data = merge_result(data, val, read_size, data_offset); data_offset += read_size; if ( data_offset == size ) break; ASSERT(data_offset < size); } if ( data_offset < size ) { /* Tailing gap, read the remaining. */ uint32_t tmp_data = vpci_read_hw(sbdf, reg + data_offset, size - data_offset); data = merge_result(data, tmp_data, size - data_offset, data_offset); } spin_unlock(&pdev->vpci->lock); return data & (0xffffffff >> (32 - 8 * size)); } /* * Perform a maybe partial write to a register. * * Note that this will only work for simple registers, if Xen needs to * trap accesses to rw1c registers (like the status PCI header register) * the logic in vpci_write will have to be expanded in order to correctly * deal with them. */ static void vpci_write_helper(const struct pci_dev *pdev, const struct vpci_register *r, unsigned int size, unsigned int offset, uint32_t data) { ASSERT(size <= r->size); if ( size != r->size ) { uint32_t val; val = r->read(pdev, r->offset, r->private); data = merge_result(val, data, size, offset); } r->write(pdev, r->offset, data & (0xffffffff >> (32 - 8 * r->size)), r->private); } void vpci_write(pci_sbdf_t sbdf, unsigned int reg, unsigned int size, uint32_t data) { const struct domain *d = current->domain; const struct pci_dev *pdev; const struct vpci_register *r; unsigned int data_offset = 0; const unsigned long *ro_map = pci_get_ro_map(sbdf.seg); if ( !size ) { ASSERT_UNREACHABLE(); return; } if ( ro_map && test_bit(sbdf.bdf, ro_map) ) /* Ignore writes to read-only devices. */ return; /* * Find the PCI dev matching the address. * Passthrough everything that's not trapped. */ pdev = pci_get_pdev_by_domain(d, sbdf.seg, sbdf.bus, sbdf.devfn); if ( !pdev ) { vpci_write_hw(sbdf, reg, size, data); return; } spin_lock(&pdev->vpci->lock); /* Write the value to the hardware or emulated registers. */ list_for_each_entry ( r, &pdev->vpci->handlers, node ) { const struct vpci_register emu = { .offset = reg + data_offset, .size = size - data_offset }; int cmp = vpci_register_cmp(&emu, r); unsigned int write_size; if ( cmp < 0 ) break; if ( cmp > 0 ) continue; if ( emu.offset < r->offset ) { /* Heading gap, write partial content to hardware. */ vpci_write_hw(sbdf, emu.offset, r->offset - emu.offset, data >> (data_offset * 8)); data_offset += r->offset - emu.offset; } /* Find the intersection size between the two sets. */ write_size = min(emu.offset + emu.size, r->offset + r->size) - max(emu.offset, r->offset); vpci_write_helper(pdev, r, write_size, reg + data_offset - r->offset, data >> (data_offset * 8)); data_offset += write_size; if ( data_offset == size ) break; ASSERT(data_offset < size); } if ( data_offset < size ) /* Tailing gap, write the remaining. */ vpci_write_hw(sbdf, reg + data_offset, size - data_offset, data >> (data_offset * 8)); spin_unlock(&pdev->vpci->lock); } /* * Local variables: * mode: C * c-file-style: "BSD" * c-basic-offset: 4 * tab-width: 4 * indent-tabs-mode: nil * End: */