diff --git a/sys/arm64/iommu/iommu.c b/sys/arm64/iommu/iommu.c index e7b5b2298e26..0fe59c5d10b6 100644 --- a/sys/arm64/iommu/iommu.c +++ b/sys/arm64/iommu/iommu.c @@ -1,528 +1,525 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2020 Ruslan Bukin * * This software was developed by SRI International and the University of * Cambridge Computer Laboratory (Department of Computer Science and * Technology) under DARPA contract HR0011-18-C-0016 ("ECATS"), as part of the * DARPA SSITH research programme. * * Portions of this work was supported by Innovate UK project 105694, * "Digital Security by Design (DSbD) Technology Platform Prototype". * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include "opt_platform.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef FDT #include #include #include #endif #include "iommu.h" #include "iommu_if.h" static MALLOC_DEFINE(M_IOMMU, "IOMMU", "IOMMU framework"); #define IOMMU_LIST_LOCK() sx_xlock(&iommu_sx) #define IOMMU_LIST_UNLOCK() sx_xunlock(&iommu_sx) #define IOMMU_LIST_ASSERT_LOCKED() sx_assert(&iommu_sx, SA_XLOCKED) #define dprintf(fmt, ...) static struct sx iommu_sx; struct iommu_entry { struct iommu_unit *iommu; LIST_ENTRY(iommu_entry) next; }; static LIST_HEAD(, iommu_entry) iommu_list = LIST_HEAD_INITIALIZER(iommu_list); static int -iommu_domain_unmap_buf(struct iommu_domain *iodom, iommu_gaddr_t base, - iommu_gaddr_t size, int flags) +iommu_domain_unmap_buf(struct iommu_domain *iodom, + struct iommu_map_entry *entry, int flags) { struct iommu_unit *iommu; int error; iommu = iodom->iommu; - - error = IOMMU_UNMAP(iommu->dev, iodom, base, size); - + error = IOMMU_UNMAP(iommu->dev, iodom, entry->start, entry->end - + entry->start); return (error); } static int -iommu_domain_map_buf(struct iommu_domain *iodom, iommu_gaddr_t base, - iommu_gaddr_t size, vm_page_t *ma, uint64_t eflags, int flags) +iommu_domain_map_buf(struct iommu_domain *iodom, struct iommu_map_entry *entry, + vm_page_t *ma, uint64_t eflags, int flags) { struct iommu_unit *iommu; vm_prot_t prot; vm_offset_t va; int error; dprintf("%s: base %lx, size %lx\n", __func__, base, size); prot = 0; if (eflags & IOMMU_MAP_ENTRY_READ) prot |= VM_PROT_READ; if (eflags & IOMMU_MAP_ENTRY_WRITE) prot |= VM_PROT_WRITE; - va = base; - + va = entry->start; iommu = iodom->iommu; - - error = IOMMU_MAP(iommu->dev, iodom, va, ma, size, prot); - + error = IOMMU_MAP(iommu->dev, iodom, va, ma, entry->end - + entry->start, prot); return (error); } static const struct iommu_domain_map_ops domain_map_ops = { .map = iommu_domain_map_buf, .unmap = iommu_domain_unmap_buf, }; static struct iommu_domain * iommu_domain_alloc(struct iommu_unit *iommu) { struct iommu_domain *iodom; iodom = IOMMU_DOMAIN_ALLOC(iommu->dev, iommu); if (iodom == NULL) return (NULL); KASSERT(iodom->end != 0, ("domain end is not set")); iommu_domain_init(iommu, iodom, &domain_map_ops); iodom->iommu = iommu; iommu_gas_init_domain(iodom); return (iodom); } static int iommu_domain_free(struct iommu_domain *iodom) { struct iommu_unit *iommu; iommu = iodom->iommu; IOMMU_LOCK(iommu); if ((iodom->flags & IOMMU_DOMAIN_GAS_INITED) != 0) { IOMMU_DOMAIN_LOCK(iodom); iommu_gas_fini_domain(iodom); IOMMU_DOMAIN_UNLOCK(iodom); } iommu_domain_fini(iodom); IOMMU_DOMAIN_FREE(iommu->dev, iodom); IOMMU_UNLOCK(iommu); return (0); } static void iommu_tag_init(struct iommu_domain *iodom, struct bus_dma_tag_iommu *t) { bus_addr_t maxaddr; maxaddr = MIN(iodom->end, BUS_SPACE_MAXADDR); t->common.impl = &bus_dma_iommu_impl; t->common.alignment = 1; t->common.boundary = 0; t->common.lowaddr = maxaddr; t->common.highaddr = maxaddr; t->common.maxsize = maxaddr; t->common.nsegments = BUS_SPACE_UNRESTRICTED; t->common.maxsegsz = maxaddr; } static struct iommu_ctx * iommu_ctx_alloc(device_t requester, struct iommu_domain *iodom, bool disabled) { struct iommu_unit *iommu; struct iommu_ctx *ioctx; iommu = iodom->iommu; ioctx = IOMMU_CTX_ALLOC(iommu->dev, iodom, requester, disabled); if (ioctx == NULL) return (NULL); ioctx->domain = iodom; return (ioctx); } static int iommu_ctx_init(device_t requester, struct iommu_ctx *ioctx) { struct bus_dma_tag_iommu *tag; struct iommu_domain *iodom; struct iommu_unit *iommu; int error; iodom = ioctx->domain; iommu = iodom->iommu; error = IOMMU_CTX_INIT(iommu->dev, ioctx); if (error) return (error); tag = ioctx->tag = malloc(sizeof(struct bus_dma_tag_iommu), M_IOMMU, M_WAITOK | M_ZERO); tag->owner = requester; tag->ctx = ioctx; tag->ctx->domain = iodom; iommu_tag_init(iodom, tag); return (error); } static struct iommu_unit * iommu_lookup(device_t dev) { struct iommu_entry *entry; struct iommu_unit *iommu; IOMMU_LIST_LOCK(); LIST_FOREACH(entry, &iommu_list, next) { iommu = entry->iommu; if (iommu->dev == dev) { IOMMU_LIST_UNLOCK(); return (iommu); } } IOMMU_LIST_UNLOCK(); return (NULL); } #ifdef FDT struct iommu_ctx * iommu_get_ctx_ofw(device_t dev, int channel) { struct iommu_domain *iodom; struct iommu_unit *iommu; struct iommu_ctx *ioctx; phandle_t node, parent; device_t iommu_dev; pcell_t *cells; int niommus; int ncells; int error; node = ofw_bus_get_node(dev); if (node <= 0) { device_printf(dev, "%s called on not ofw based device.\n", __func__); return (NULL); } error = ofw_bus_parse_xref_list_get_length(node, "iommus", "#iommu-cells", &niommus); if (error) { device_printf(dev, "%s can't get iommu list.\n", __func__); return (NULL); } if (niommus == 0) { device_printf(dev, "%s iommu list is empty.\n", __func__); return (NULL); } error = ofw_bus_parse_xref_list_alloc(node, "iommus", "#iommu-cells", channel, &parent, &ncells, &cells); if (error != 0) { device_printf(dev, "%s can't get iommu device xref.\n", __func__); return (NULL); } iommu_dev = OF_device_from_xref(parent); if (iommu_dev == NULL) { device_printf(dev, "%s can't get iommu device.\n", __func__); return (NULL); } iommu = iommu_lookup(iommu_dev); if (iommu == NULL) { device_printf(dev, "%s can't lookup iommu.\n", __func__); return (NULL); } /* * In our current configuration we have a domain per each ctx, * so allocate a domain first. */ iodom = iommu_domain_alloc(iommu); if (iodom == NULL) { device_printf(dev, "%s can't allocate domain.\n", __func__); return (NULL); } ioctx = iommu_ctx_alloc(dev, iodom, false); if (ioctx == NULL) { iommu_domain_free(iodom); return (NULL); } ioctx->domain = iodom; error = IOMMU_OFW_MD_DATA(iommu->dev, ioctx, cells, ncells); if (error) { device_printf(dev, "%s can't set MD data\n", __func__); return (NULL); } error = iommu_ctx_init(dev, ioctx); if (error) { IOMMU_CTX_FREE(iommu->dev, ioctx); iommu_domain_free(iodom); return (NULL); } return (ioctx); } #endif struct iommu_ctx * iommu_get_ctx(struct iommu_unit *iommu, device_t requester, uint16_t rid, bool disabled, bool rmrr) { struct iommu_domain *iodom; struct iommu_ctx *ioctx; int error; IOMMU_LOCK(iommu); ioctx = IOMMU_CTX_LOOKUP(iommu->dev, requester); if (ioctx) { IOMMU_UNLOCK(iommu); return (ioctx); } IOMMU_UNLOCK(iommu); /* * In our current configuration we have a domain per each ctx. * So allocate a domain first. */ iodom = iommu_domain_alloc(iommu); if (iodom == NULL) return (NULL); ioctx = iommu_ctx_alloc(requester, iodom, disabled); if (ioctx == NULL) { iommu_domain_free(iodom); return (NULL); } error = iommu_ctx_init(requester, ioctx); if (error) { IOMMU_CTX_FREE(iommu->dev, ioctx); iommu_domain_free(iodom); return (NULL); } return (ioctx); } void iommu_free_ctx_locked(struct iommu_unit *iommu, struct iommu_ctx *ioctx) { struct bus_dma_tag_iommu *tag; IOMMU_ASSERT_LOCKED(iommu); tag = ioctx->tag; IOMMU_CTX_FREE(iommu->dev, ioctx); free(tag, M_IOMMU); } void iommu_free_ctx(struct iommu_ctx *ioctx) { struct iommu_unit *iommu; struct iommu_domain *iodom; int error; iodom = ioctx->domain; iommu = iodom->iommu; IOMMU_LOCK(iommu); iommu_free_ctx_locked(iommu, ioctx); IOMMU_UNLOCK(iommu); /* Since we have a domain per each ctx, remove the domain too. */ error = iommu_domain_free(iodom); if (error) device_printf(iommu->dev, "Could not free a domain\n"); } static void iommu_domain_free_entry(struct iommu_map_entry *entry, bool free) { iommu_gas_free_space(entry); if (free) iommu_gas_free_entry(entry); else entry->flags = 0; } void iommu_domain_unload(struct iommu_domain *iodom, struct iommu_map_entries_tailq *entries, bool cansleep) { struct iommu_map_entry *entry, *entry1; int error __diagused; TAILQ_FOREACH_SAFE(entry, entries, dmamap_link, entry1) { KASSERT((entry->flags & IOMMU_MAP_ENTRY_MAP) != 0, ("not mapped entry %p %p", iodom, entry)); - error = iodom->ops->unmap(iodom, entry->start, entry->end - - entry->start, cansleep ? IOMMU_PGF_WAITOK : 0); + error = iodom->ops->unmap(iodom, entry, + cansleep ? IOMMU_PGF_WAITOK : 0); KASSERT(error == 0, ("unmap %p error %d", iodom, error)); TAILQ_REMOVE(entries, entry, dmamap_link); iommu_domain_free_entry(entry, true); } if (TAILQ_EMPTY(entries)) return; panic("entries map is not empty"); } int iommu_register(struct iommu_unit *iommu) { struct iommu_entry *entry; mtx_init(&iommu->lock, "IOMMU", NULL, MTX_DEF); entry = malloc(sizeof(struct iommu_entry), M_IOMMU, M_WAITOK | M_ZERO); entry->iommu = iommu; IOMMU_LIST_LOCK(); LIST_INSERT_HEAD(&iommu_list, entry, next); IOMMU_LIST_UNLOCK(); sysctl_ctx_init(&iommu->sysctl_ctx); iommu_init_busdma(iommu); return (0); } int iommu_unregister(struct iommu_unit *iommu) { struct iommu_entry *entry, *tmp; IOMMU_LIST_LOCK(); LIST_FOREACH_SAFE(entry, &iommu_list, next, tmp) { if (entry->iommu == iommu) { LIST_REMOVE(entry, next); free(entry, M_IOMMU); } } IOMMU_LIST_UNLOCK(); iommu_fini_busdma(iommu); sysctl_ctx_free(&iommu->sysctl_ctx); mtx_destroy(&iommu->lock); return (0); } struct iommu_unit * iommu_find(device_t dev, bool verbose) { struct iommu_entry *entry; struct iommu_unit *iommu; int error; IOMMU_LIST_LOCK(); LIST_FOREACH(entry, &iommu_list, next) { iommu = entry->iommu; error = IOMMU_FIND(iommu->dev, dev); if (error == 0) { IOMMU_LIST_UNLOCK(); return (entry->iommu); } } IOMMU_LIST_UNLOCK(); return (NULL); } void iommu_unit_pre_instantiate_ctx(struct iommu_unit *unit) { } void iommu_domain_unload_entry(struct iommu_map_entry *entry, bool free, bool cansleep __unused) { dprintf("%s\n", __func__); iommu_domain_free_entry(entry, free); } static void iommu_init(void) { sx_init(&iommu_sx, "IOMMU list"); } SYSINIT(iommu, SI_SUB_DRIVERS, SI_ORDER_FIRST, iommu_init, NULL); diff --git a/sys/dev/iommu/iommu.h b/sys/dev/iommu/iommu.h index 957d4e14e8e2..84d8c3680b71 100644 --- a/sys/dev/iommu/iommu.h +++ b/sys/dev/iommu/iommu.h @@ -1,204 +1,204 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2013 The FreeBSD Foundation * * This software was developed by Konstantin Belousov * under sponsorship from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #ifndef _DEV_IOMMU_IOMMU_H_ #define _DEV_IOMMU_IOMMU_H_ #include struct bus_dma_tag_common; struct iommu_map_entry; TAILQ_HEAD(iommu_map_entries_tailq, iommu_map_entry); RB_HEAD(iommu_gas_entries_tree, iommu_map_entry); RB_PROTOTYPE(iommu_gas_entries_tree, iommu_map_entry, rb_entry, iommu_gas_cmp_entries); struct iommu_qi_genseq { u_int gen; uint32_t seq; }; struct iommu_map_entry { iommu_gaddr_t start; iommu_gaddr_t end; iommu_gaddr_t first; /* Least start in subtree */ iommu_gaddr_t last; /* Greatest end in subtree */ iommu_gaddr_t free_down; /* Max free space below the current R/B tree node */ u_int flags; union { TAILQ_ENTRY(iommu_map_entry) dmamap_link; /* DMA map entries */ struct iommu_map_entry *tlb_flush_next; }; RB_ENTRY(iommu_map_entry) rb_entry; /* Links for domain entries */ struct iommu_domain *domain; struct iommu_qi_genseq gseq; }; struct iommu_unit { struct mtx lock; device_t dev; int unit; struct sysctl_ctx_list sysctl_ctx; int dma_enabled; /* Busdma delayed map load */ struct task dmamap_load_task; TAILQ_HEAD(, bus_dmamap_iommu) delayed_maps; struct taskqueue *delayed_taskqueue; /* * Bitmap of buses for which context must ignore slot:func, * duplicating the page table pointer into all context table * entries. This is a client-controlled quirk to support some * NTBs. */ uint32_t buswide_ctxs[(PCI_BUSMAX + 1) / NBBY / sizeof(uint32_t)]; }; struct iommu_domain_map_ops { - int (*map)(struct iommu_domain *domain, iommu_gaddr_t base, - iommu_gaddr_t size, vm_page_t *ma, uint64_t pflags, int flags); - int (*unmap)(struct iommu_domain *domain, iommu_gaddr_t base, - iommu_gaddr_t size, int flags); + int (*map)(struct iommu_domain *domain, struct iommu_map_entry *entry, + vm_page_t *ma, uint64_t pflags, int flags); + int (*unmap)(struct iommu_domain *domain, struct iommu_map_entry *entry, + int flags); }; /* * Locking annotations: * (u) - Protected by iommu unit lock * (d) - Protected by domain lock * (c) - Immutable after initialization */ struct iommu_domain { struct iommu_unit *iommu; /* (c) */ const struct iommu_domain_map_ops *ops; struct mtx lock; /* (c) */ struct task unload_task; /* (c) */ u_int entries_cnt; /* (d) */ struct iommu_map_entries_tailq unload_entries; /* (d) Entries to unload */ struct iommu_gas_entries_tree rb_root; /* (d) */ struct iommu_map_entry *start_gap; /* (d) */ iommu_gaddr_t end; /* (c) Highest address + 1 in the guest AS */ struct iommu_map_entry *first_place, *last_place; /* (d) */ struct iommu_map_entry *msi_entry; /* (d) Arch-specific */ iommu_gaddr_t msi_base; /* (d) Arch-specific */ vm_paddr_t msi_phys; /* (d) Arch-specific */ u_int flags; /* (u) */ }; struct iommu_ctx { struct iommu_domain *domain; /* (c) */ struct bus_dma_tag_iommu *tag; /* (c) Root tag */ u_long loads; /* atomic updates, for stat only */ u_long unloads; /* same */ u_int flags; /* (u) */ uint16_t rid; /* (c) pci RID */ }; /* struct iommu_ctx flags */ #define IOMMU_CTX_FAULTED 0x0001 /* Fault was reported, last_fault_rec is valid */ #define IOMMU_CTX_DISABLED 0x0002 /* Device is disabled, the ephemeral reference is kept to prevent context destruction */ #define IOMMU_DOMAIN_GAS_INITED 0x0001 #define IOMMU_DOMAIN_PGTBL_INITED 0x0002 #define IOMMU_DOMAIN_IDMAP 0x0010 /* Domain uses identity page table */ #define IOMMU_DOMAIN_RMRR 0x0020 /* Domain contains RMRR entry, cannot be turned off */ #define IOMMU_LOCK(unit) mtx_lock(&(unit)->lock) #define IOMMU_UNLOCK(unit) mtx_unlock(&(unit)->lock) #define IOMMU_ASSERT_LOCKED(unit) mtx_assert(&(unit)->lock, MA_OWNED) #define IOMMU_DOMAIN_LOCK(dom) mtx_lock(&(dom)->lock) #define IOMMU_DOMAIN_UNLOCK(dom) mtx_unlock(&(dom)->lock) #define IOMMU_DOMAIN_ASSERT_LOCKED(dom) mtx_assert(&(dom)->lock, MA_OWNED) void iommu_free_ctx(struct iommu_ctx *ctx); void iommu_free_ctx_locked(struct iommu_unit *iommu, struct iommu_ctx *ctx); struct iommu_ctx *iommu_get_ctx(struct iommu_unit *, device_t dev, uint16_t rid, bool id_mapped, bool rmrr_init); struct iommu_unit *iommu_find(device_t dev, bool verbose); void iommu_domain_unload_entry(struct iommu_map_entry *entry, bool free, bool cansleep); void iommu_domain_unload(struct iommu_domain *domain, struct iommu_map_entries_tailq *entries, bool cansleep); void iommu_unit_pre_instantiate_ctx(struct iommu_unit *iommu); struct iommu_ctx *iommu_instantiate_ctx(struct iommu_unit *iommu, device_t dev, bool rmrr); device_t iommu_get_requester(device_t dev, uint16_t *rid); int iommu_init_busdma(struct iommu_unit *unit); void iommu_fini_busdma(struct iommu_unit *unit); void iommu_gas_init_domain(struct iommu_domain *domain); void iommu_gas_fini_domain(struct iommu_domain *domain); struct iommu_map_entry *iommu_gas_alloc_entry(struct iommu_domain *domain, u_int flags); void iommu_gas_free_entry(struct iommu_map_entry *entry); void iommu_gas_free_space(struct iommu_map_entry *entry); void iommu_gas_remove(struct iommu_domain *domain, iommu_gaddr_t start, iommu_gaddr_t size); int iommu_gas_map(struct iommu_domain *domain, const struct bus_dma_tag_common *common, iommu_gaddr_t size, int offset, u_int eflags, u_int flags, vm_page_t *ma, struct iommu_map_entry **res); void iommu_gas_free_region(struct iommu_map_entry *entry); int iommu_gas_map_region(struct iommu_domain *domain, struct iommu_map_entry *entry, u_int eflags, u_int flags, vm_page_t *ma); int iommu_gas_reserve_region(struct iommu_domain *domain, iommu_gaddr_t start, iommu_gaddr_t end, struct iommu_map_entry **entry0); int iommu_gas_reserve_region_extend(struct iommu_domain *domain, iommu_gaddr_t start, iommu_gaddr_t end); void iommu_set_buswide_ctx(struct iommu_unit *unit, u_int busno); bool iommu_is_buswide_ctx(struct iommu_unit *unit, u_int busno); void iommu_domain_init(struct iommu_unit *unit, struct iommu_domain *domain, const struct iommu_domain_map_ops *ops); void iommu_domain_fini(struct iommu_domain *domain); bool bus_dma_iommu_set_buswide(device_t dev); int bus_dma_iommu_load_ident(bus_dma_tag_t dmat, bus_dmamap_t map, vm_paddr_t start, vm_size_t length, int flags); bus_dma_tag_t iommu_get_dma_tag(device_t dev, device_t child); struct iommu_ctx *iommu_get_dev_ctx(device_t dev); struct iommu_domain *iommu_get_ctx_domain(struct iommu_ctx *ctx); SYSCTL_DECL(_hw_iommu); #endif /* !_DEV_IOMMU_IOMMU_H_ */ diff --git a/sys/dev/iommu/iommu_gas.c b/sys/dev/iommu/iommu_gas.c index 4b6141b981da..26ac38da3c4f 100644 --- a/sys/dev/iommu/iommu_gas.c +++ b/sys/dev/iommu/iommu_gas.c @@ -1,1108 +1,1106 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2013 The FreeBSD Foundation * * This software was developed by Konstantin Belousov * under sponsorship from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #define RB_AUGMENT_CHECK(entry) iommu_gas_augment_entry(entry) #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* * Guest Address Space management. */ static uma_zone_t iommu_map_entry_zone; #ifdef INVARIANTS static int iommu_check_free; #endif static void intel_gas_init(void) { iommu_map_entry_zone = uma_zcreate("IOMMU_MAP_ENTRY", sizeof(struct iommu_map_entry), NULL, NULL, NULL, NULL, UMA_ALIGN_PTR, UMA_ZONE_NODUMP); } SYSINIT(intel_gas, SI_SUB_DRIVERS, SI_ORDER_FIRST, intel_gas_init, NULL); struct iommu_map_entry * iommu_gas_alloc_entry(struct iommu_domain *domain, u_int flags) { struct iommu_map_entry *res; KASSERT((flags & ~(IOMMU_PGF_WAITOK)) == 0, ("unsupported flags %x", flags)); res = uma_zalloc(iommu_map_entry_zone, ((flags & IOMMU_PGF_WAITOK) != 0 ? M_WAITOK : M_NOWAIT) | M_ZERO); if (res != NULL && domain != NULL) { res->domain = domain; atomic_add_int(&domain->entries_cnt, 1); } return (res); } void iommu_gas_free_entry(struct iommu_map_entry *entry) { struct iommu_domain *domain; domain = entry->domain; if (domain != NULL) atomic_subtract_int(&domain->entries_cnt, 1); uma_zfree(iommu_map_entry_zone, entry); } static int iommu_gas_cmp_entries(struct iommu_map_entry *a, struct iommu_map_entry *b) { /* First and last entries have zero size, so <= */ KASSERT(a->start <= a->end, ("inverted entry %p (%jx, %jx)", a, (uintmax_t)a->start, (uintmax_t)a->end)); KASSERT(b->start <= b->end, ("inverted entry %p (%jx, %jx)", b, (uintmax_t)b->start, (uintmax_t)b->end)); KASSERT(((a->flags | b->flags) & IOMMU_MAP_ENTRY_FAKE) != 0 || a->end <= b->start || b->end <= a->start || a->end == a->start || b->end == b->start, ("overlapping entries %p (%jx, %jx) f %#x %p (%jx, %jx) f %#x" " domain %p %p", a, (uintmax_t)a->start, (uintmax_t)a->end, a->flags, b, (uintmax_t)b->start, (uintmax_t)b->end, b->flags, a->domain, b->domain)); if (a->end < b->end) return (-1); else if (b->end < a->end) return (1); return (0); } /* * Update augmentation data based on data from children. * Return true if and only if the update changes the augmentation data. */ static bool iommu_gas_augment_entry(struct iommu_map_entry *entry) { struct iommu_map_entry *child; iommu_gaddr_t bound, delta, free_down; free_down = 0; bound = entry->start; if ((child = RB_LEFT(entry, rb_entry)) != NULL) { free_down = MAX(child->free_down, bound - child->last); bound = child->first; } delta = bound - entry->first; entry->first = bound; bound = entry->end; if ((child = RB_RIGHT(entry, rb_entry)) != NULL) { free_down = MAX(free_down, child->free_down); free_down = MAX(free_down, child->first - bound); bound = child->last; } delta += entry->last - bound; if (delta == 0) delta = entry->free_down - free_down; entry->last = bound; entry->free_down = free_down; /* * Return true either if the value of last-first changed, * or if free_down changed. */ return (delta != 0); } RB_GENERATE(iommu_gas_entries_tree, iommu_map_entry, rb_entry, iommu_gas_cmp_entries); #ifdef INVARIANTS static void iommu_gas_check_free(struct iommu_domain *domain) { struct iommu_map_entry *entry, *l, *r; iommu_gaddr_t v; RB_FOREACH(entry, iommu_gas_entries_tree, &domain->rb_root) { KASSERT(domain == entry->domain, ("mismatched free domain %p entry %p entry->domain %p", domain, entry, entry->domain)); l = RB_LEFT(entry, rb_entry); r = RB_RIGHT(entry, rb_entry); v = 0; if (l != NULL) { v = MAX(v, l->free_down); v = MAX(v, entry->start - l->last); } if (r != NULL) { v = MAX(v, r->free_down); v = MAX(v, r->first - entry->end); } MPASS(entry->free_down == v); } } #endif static void iommu_gas_rb_remove(struct iommu_domain *domain, struct iommu_map_entry *entry) { struct iommu_map_entry *nbr; /* Removing entry may open a new free gap before domain->start_gap. */ if (entry->end <= domain->start_gap->end) { if (RB_RIGHT(entry, rb_entry) != NULL) nbr = iommu_gas_entries_tree_RB_NEXT(entry); else if (RB_LEFT(entry, rb_entry) != NULL) nbr = RB_LEFT(entry, rb_entry); else nbr = RB_PARENT(entry, rb_entry); domain->start_gap = nbr; } RB_REMOVE(iommu_gas_entries_tree, &domain->rb_root, entry); } struct iommu_domain * iommu_get_ctx_domain(struct iommu_ctx *ctx) { return (ctx->domain); } void iommu_gas_init_domain(struct iommu_domain *domain) { struct iommu_map_entry *begin, *end; begin = iommu_gas_alloc_entry(domain, IOMMU_PGF_WAITOK); end = iommu_gas_alloc_entry(domain, IOMMU_PGF_WAITOK); IOMMU_DOMAIN_LOCK(domain); KASSERT(domain->entries_cnt == 2, ("dirty domain %p", domain)); KASSERT(RB_EMPTY(&domain->rb_root), ("non-empty entries %p", domain)); end->start = domain->end; end->end = domain->end; end->flags = IOMMU_MAP_ENTRY_PLACE | IOMMU_MAP_ENTRY_UNMAPPED; RB_INSERT(iommu_gas_entries_tree, &domain->rb_root, end); begin->start = 0; begin->end = 0; begin->flags = IOMMU_MAP_ENTRY_PLACE | IOMMU_MAP_ENTRY_UNMAPPED; RB_INSERT_PREV(iommu_gas_entries_tree, &domain->rb_root, end, begin); iommu_gas_augment_entry(end); iommu_gas_augment_entry(begin); domain->start_gap = begin; domain->first_place = begin; domain->last_place = end; domain->flags |= IOMMU_DOMAIN_GAS_INITED; IOMMU_DOMAIN_UNLOCK(domain); } void iommu_gas_fini_domain(struct iommu_domain *domain) { struct iommu_map_entry *entry; IOMMU_DOMAIN_ASSERT_LOCKED(domain); KASSERT(domain->entries_cnt == 2, ("domain still in use %p", domain)); entry = RB_MIN(iommu_gas_entries_tree, &domain->rb_root); KASSERT(entry->start == 0, ("start entry start %p", domain)); KASSERT(entry->end == IOMMU_PAGE_SIZE, ("start entry end %p", domain)); KASSERT(entry->flags == (IOMMU_MAP_ENTRY_PLACE | IOMMU_MAP_ENTRY_UNMAPPED), ("start entry flags %p", domain)); iommu_gas_rb_remove(domain, entry); iommu_gas_free_entry(entry); entry = RB_MAX(iommu_gas_entries_tree, &domain->rb_root); KASSERT(entry->start == domain->end, ("end entry start %p", domain)); KASSERT(entry->end == domain->end, ("end entry end %p", domain)); KASSERT(entry->flags == (IOMMU_MAP_ENTRY_PLACE | IOMMU_MAP_ENTRY_UNMAPPED), ("end entry flags %p", domain)); iommu_gas_rb_remove(domain, entry); iommu_gas_free_entry(entry); } struct iommu_gas_match_args { iommu_gaddr_t size; int offset; const struct bus_dma_tag_common *common; u_int gas_flags; struct iommu_map_entry *entry; }; /* * The interval [beg, end) is a free interval between two iommu_map_entries. * Addresses can be allocated only in the range [lbound, ubound]. Try to * allocate space in the free interval, subject to the conditions expressed by * a, and return 'true' if and only if the allocation attempt succeeds. */ static bool iommu_gas_match_one(struct iommu_gas_match_args *a, iommu_gaddr_t beg, iommu_gaddr_t end, iommu_gaddr_t lbound, iommu_gaddr_t ubound) { struct iommu_map_entry *entry; iommu_gaddr_t first, size, start; int offset; /* * The prev->end is always aligned on the page size, which * causes page alignment for the entry->start too. * * Create IOMMU_PAGE_SIZE gaps before, after new entry * to ensure that out-of-bounds accesses fault. */ beg = MAX(beg + IOMMU_PAGE_SIZE, lbound); start = roundup2(beg, a->common->alignment); if (start < beg) return (false); if (end < IOMMU_PAGE_SIZE + 1) return (false); end = MIN(end - IOMMU_PAGE_SIZE - 1, ubound); offset = a->offset; size = a->size; if (start + offset + size - 1 > end) return (false); /* Check for and try to skip past boundary crossing. */ if (!vm_addr_bound_ok(start + offset, size, a->common->boundary)) { /* * The start + offset to start + offset + size region crosses * the boundary. Check if there is enough space after the next * boundary after the beg. */ first = start; beg = roundup2(start + offset + 1, a->common->boundary); start = roundup2(beg, a->common->alignment); if (start + offset + size - 1 > end || !vm_addr_bound_ok(start + offset, size, a->common->boundary)) { /* * Not enough space to align at the requested boundary, * or boundary is smaller than the size, but allowed to * split. We already checked that start + size does not * overlap ubound. * * XXXKIB. It is possible that beg is exactly at the * start of the next entry, then we do not have gap. * Ignore for now. */ if ((a->gas_flags & IOMMU_MF_CANSPLIT) == 0) return (false); size = beg - first - offset; start = first; } } entry = a->entry; entry->start = start; entry->end = start + roundup2(size + offset, IOMMU_PAGE_SIZE); entry->flags = IOMMU_MAP_ENTRY_MAP; return (true); } /* Find the next entry that might abut a big-enough range. */ static struct iommu_map_entry * iommu_gas_next(struct iommu_map_entry *curr, iommu_gaddr_t min_free) { struct iommu_map_entry *next; if ((next = RB_RIGHT(curr, rb_entry)) != NULL && next->free_down >= min_free) { /* Find next entry in right subtree. */ do curr = next; while ((next = RB_LEFT(curr, rb_entry)) != NULL && next->free_down >= min_free); } else { /* Find next entry in a left-parent ancestor. */ while ((next = RB_PARENT(curr, rb_entry)) != NULL && curr == RB_RIGHT(next, rb_entry)) curr = next; curr = next; } return (curr); } /* * Address-ordered first-fit search of 'domain' for free space satisfying the * conditions of 'a'. The space allocated is at least one page big, and is * bounded by guard pages to the left and right. The allocated space for * 'domain' is described by an rb-tree of map entries at domain->rb_root, and * domain->start_gap points to a map entry less than or adjacent to the first * free-space of size at least 3 pages. */ static int iommu_gas_find_space(struct iommu_domain *domain, struct iommu_gas_match_args *a) { struct iommu_map_entry *curr, *first; iommu_gaddr_t addr, min_free; IOMMU_DOMAIN_ASSERT_LOCKED(domain); KASSERT(a->entry->flags == 0, ("dirty entry %p %p", domain, a->entry)); /* * start_gap may point to an entry adjacent to gaps too small for any * new allocation. In that case, advance start_gap to the first free * space big enough for a minimum allocation plus two guard pages. */ min_free = 3 * IOMMU_PAGE_SIZE; first = domain->start_gap; while (first != NULL && first->free_down < min_free) first = RB_PARENT(first, rb_entry); for (curr = first; curr != NULL; curr = iommu_gas_next(curr, min_free)) { if ((first = RB_LEFT(curr, rb_entry)) != NULL && first->last + min_free <= curr->start) break; if ((first = RB_RIGHT(curr, rb_entry)) != NULL && curr->end + min_free <= first->first) break; } domain->start_gap = curr; /* * If the subtree doesn't have free space for the requested allocation * plus two guard pages, skip it. */ min_free = 2 * IOMMU_PAGE_SIZE + roundup2(a->size + a->offset, IOMMU_PAGE_SIZE); /* Climb to find a node in the subtree of big-enough ranges. */ first = curr; while (first != NULL && first->free_down < min_free) first = RB_PARENT(first, rb_entry); /* * Walk the big-enough ranges tree until one satisfies alignment * requirements, or violates lowaddr address requirement. */ addr = a->common->lowaddr; for (curr = first; curr != NULL; curr = iommu_gas_next(curr, min_free)) { if ((first = RB_LEFT(curr, rb_entry)) != NULL && iommu_gas_match_one(a, first->last, curr->start, 0, addr)) { RB_INSERT_PREV(iommu_gas_entries_tree, &domain->rb_root, curr, a->entry); return (0); } if (curr->end >= addr) { /* All remaining ranges > addr */ break; } if ((first = RB_RIGHT(curr, rb_entry)) != NULL && iommu_gas_match_one(a, curr->end, first->first, 0, addr)) { RB_INSERT_NEXT(iommu_gas_entries_tree, &domain->rb_root, curr, a->entry); return (0); } } /* * To resume the search at the start of the upper region, first climb to * the nearest ancestor that spans highaddr. Then find the last entry * before highaddr that could abut a big-enough range. */ addr = a->common->highaddr; while (curr != NULL && curr->last < addr) curr = RB_PARENT(curr, rb_entry); first = NULL; while (curr != NULL && curr->free_down >= min_free) { if (addr < curr->end) curr = RB_LEFT(curr, rb_entry); else { first = curr; curr = RB_RIGHT(curr, rb_entry); } } /* * Walk the remaining big-enough ranges until one satisfies alignment * requirements. */ for (curr = first; curr != NULL; curr = iommu_gas_next(curr, min_free)) { if ((first = RB_LEFT(curr, rb_entry)) != NULL && iommu_gas_match_one(a, first->last, curr->start, addr + 1, domain->end - 1)) { RB_INSERT_PREV(iommu_gas_entries_tree, &domain->rb_root, curr, a->entry); return (0); } if ((first = RB_RIGHT(curr, rb_entry)) != NULL && iommu_gas_match_one(a, curr->end, first->first, addr + 1, domain->end - 1)) { RB_INSERT_NEXT(iommu_gas_entries_tree, &domain->rb_root, curr, a->entry); return (0); } } return (ENOMEM); } static int iommu_gas_alloc_region(struct iommu_domain *domain, struct iommu_map_entry *entry, u_int flags) { struct iommu_map_entry *next, *prev; IOMMU_DOMAIN_ASSERT_LOCKED(domain); if ((entry->start & IOMMU_PAGE_MASK) != 0 || (entry->end & IOMMU_PAGE_MASK) != 0) return (EINVAL); if (entry->start >= entry->end) return (EINVAL); if (entry->end >= domain->end) return (EINVAL); entry->flags |= IOMMU_MAP_ENTRY_FAKE; next = RB_NFIND(iommu_gas_entries_tree, &domain->rb_root, entry); KASSERT(next != NULL, ("next must be non-null %p %jx", domain, (uintmax_t)entry->start)); prev = RB_PREV(iommu_gas_entries_tree, &domain->rb_root, next); /* prev could be NULL */ entry->flags &= ~IOMMU_MAP_ENTRY_FAKE; /* * Adapt to broken BIOSes which specify overlapping RMRR * entries. * * XXXKIB: this does not handle a case when prev or next * entries are completely covered by the current one, which * extends both ways. */ if (prev != NULL && prev->end > entry->start && (prev->flags & IOMMU_MAP_ENTRY_PLACE) == 0) { if ((flags & IOMMU_MF_RMRR) == 0 || (prev->flags & IOMMU_MAP_ENTRY_RMRR) == 0) return (EBUSY); entry->start = prev->end; } if (next->start < entry->end && (next->flags & IOMMU_MAP_ENTRY_PLACE) == 0) { if ((flags & IOMMU_MF_RMRR) == 0 || (next->flags & IOMMU_MAP_ENTRY_RMRR) == 0) return (EBUSY); entry->end = next->start; } if (entry->end == entry->start) return (0); if (prev != NULL && prev->end > entry->start) { /* This assumes that prev is the placeholder entry. */ iommu_gas_rb_remove(domain, prev); prev = NULL; } RB_INSERT_PREV(iommu_gas_entries_tree, &domain->rb_root, next, entry); if (next->start < entry->end) { iommu_gas_rb_remove(domain, next); next = NULL; } if ((flags & IOMMU_MF_RMRR) != 0) entry->flags = IOMMU_MAP_ENTRY_RMRR; #ifdef INVARIANTS struct iommu_map_entry *ip, *in; ip = RB_PREV(iommu_gas_entries_tree, &domain->rb_root, entry); in = RB_NEXT(iommu_gas_entries_tree, &domain->rb_root, entry); KASSERT(prev == NULL || ip == prev, ("RMRR %p (%jx %jx) prev %p (%jx %jx) ins prev %p (%jx %jx)", entry, entry->start, entry->end, prev, prev == NULL ? 0 : prev->start, prev == NULL ? 0 : prev->end, ip, ip == NULL ? 0 : ip->start, ip == NULL ? 0 : ip->end)); KASSERT(next == NULL || in == next, ("RMRR %p (%jx %jx) next %p (%jx %jx) ins next %p (%jx %jx)", entry, entry->start, entry->end, next, next == NULL ? 0 : next->start, next == NULL ? 0 : next->end, in, in == NULL ? 0 : in->start, in == NULL ? 0 : in->end)); #endif return (0); } void iommu_gas_free_space(struct iommu_map_entry *entry) { struct iommu_domain *domain; domain = entry->domain; KASSERT((entry->flags & (IOMMU_MAP_ENTRY_PLACE | IOMMU_MAP_ENTRY_RMRR | IOMMU_MAP_ENTRY_MAP)) == IOMMU_MAP_ENTRY_MAP, ("permanent entry %p %p", domain, entry)); IOMMU_DOMAIN_LOCK(domain); iommu_gas_rb_remove(domain, entry); entry->flags &= ~IOMMU_MAP_ENTRY_MAP; #ifdef INVARIANTS if (iommu_check_free) iommu_gas_check_free(domain); #endif IOMMU_DOMAIN_UNLOCK(domain); } void iommu_gas_free_region(struct iommu_map_entry *entry) { struct iommu_domain *domain; domain = entry->domain; KASSERT((entry->flags & (IOMMU_MAP_ENTRY_PLACE | IOMMU_MAP_ENTRY_RMRR | IOMMU_MAP_ENTRY_MAP)) == IOMMU_MAP_ENTRY_RMRR, ("non-RMRR entry %p %p", domain, entry)); IOMMU_DOMAIN_LOCK(domain); if (entry != domain->first_place && entry != domain->last_place) iommu_gas_rb_remove(domain, entry); entry->flags &= ~IOMMU_MAP_ENTRY_RMRR; IOMMU_DOMAIN_UNLOCK(domain); } static struct iommu_map_entry * iommu_gas_remove_clip_left(struct iommu_domain *domain, iommu_gaddr_t start, iommu_gaddr_t end, struct iommu_map_entry **r) { struct iommu_map_entry *entry, *res, fentry; IOMMU_DOMAIN_ASSERT_LOCKED(domain); MPASS(start <= end); MPASS(end <= domain->end); /* * Find an entry which contains the supplied guest's address * start, or the first entry after the start. Since we * asserted that start is below domain end, entry should * exist. Then clip it if needed. */ bzero(&fentry, sizeof(fentry)); fentry.start = start + 1; fentry.end = start + 1; fentry.flags = IOMMU_MAP_ENTRY_FAKE; entry = RB_NFIND(iommu_gas_entries_tree, &domain->rb_root, &fentry); if (entry->start >= start || (entry->flags & IOMMU_MAP_ENTRY_RMRR) != 0) return (entry); res = *r; *r = NULL; *res = *entry; res->start = entry->end = start; RB_UPDATE_AUGMENT(entry, rb_entry); RB_INSERT_NEXT(iommu_gas_entries_tree, &domain->rb_root, entry, res); return (res); } static bool iommu_gas_remove_clip_right(struct iommu_domain *domain, iommu_gaddr_t end, struct iommu_map_entry *entry, struct iommu_map_entry *r) { if (entry->start >= end || (entry->flags & IOMMU_MAP_ENTRY_RMRR) != 0) return (false); *r = *entry; r->end = entry->start = end; RB_UPDATE_AUGMENT(entry, rb_entry); RB_INSERT_PREV(iommu_gas_entries_tree, &domain->rb_root, entry, r); return (true); } static void iommu_gas_remove_unmap(struct iommu_domain *domain, struct iommu_map_entry *entry, struct iommu_map_entries_tailq *gcp) { IOMMU_DOMAIN_ASSERT_LOCKED(domain); if ((entry->flags & (IOMMU_MAP_ENTRY_UNMAPPED | IOMMU_MAP_ENTRY_RMRR | IOMMU_MAP_ENTRY_REMOVING)) != 0) return; MPASS((entry->flags & IOMMU_MAP_ENTRY_PLACE) == 0); entry->flags |= IOMMU_MAP_ENTRY_REMOVING; TAILQ_INSERT_TAIL(gcp, entry, dmamap_link); } static void iommu_gas_remove_locked(struct iommu_domain *domain, iommu_gaddr_t start, iommu_gaddr_t size, struct iommu_map_entries_tailq *gc, struct iommu_map_entry **r1, struct iommu_map_entry **r2) { struct iommu_map_entry *entry, *nentry; iommu_gaddr_t end; IOMMU_DOMAIN_ASSERT_LOCKED(domain); end = start + size; nentry = iommu_gas_remove_clip_left(domain, start, end, r1); RB_FOREACH_FROM(entry, iommu_gas_entries_tree, nentry) { if (entry->start >= end) break; KASSERT(start <= entry->start, ("iommu_gas_remove entry (%#jx, %#jx) start %#jx", entry->start, entry->end, start)); iommu_gas_remove_unmap(domain, entry, gc); } if (iommu_gas_remove_clip_right(domain, end, entry, *r2)) { iommu_gas_remove_unmap(domain, *r2, gc); *r2 = NULL; } #ifdef INVARIANTS RB_FOREACH(entry, iommu_gas_entries_tree, &domain->rb_root) { if ((entry->flags & (IOMMU_MAP_ENTRY_RMRR | IOMMU_MAP_ENTRY_PLACE)) != 0) continue; KASSERT(entry->end <= start || entry->start >= end, ("iommu_gas_remove leftover entry (%#jx, %#jx) range " "(%#jx, %#jx)", entry->start, entry->end, start, end)); } #endif } static void iommu_gas_remove_init(struct iommu_domain *domain, struct iommu_map_entries_tailq *gc, struct iommu_map_entry **r1, struct iommu_map_entry **r2) { TAILQ_INIT(gc); *r1 = iommu_gas_alloc_entry(domain, IOMMU_PGF_WAITOK); *r2 = iommu_gas_alloc_entry(domain, IOMMU_PGF_WAITOK); } static void iommu_gas_remove_cleanup(struct iommu_domain *domain, struct iommu_map_entries_tailq *gc, struct iommu_map_entry **r1, struct iommu_map_entry **r2) { if (*r1 != NULL) { iommu_gas_free_entry(*r1); *r1 = NULL; } if (*r2 != NULL) { iommu_gas_free_entry(*r2); *r2 = NULL; } iommu_domain_unload(domain, gc, true); } /* * Remove specified range from the GAS of the domain. Note that the * removal is not guaranteed to occur upon the function return, it * might be finalized some time after, when hardware reports that * (queued) IOTLB invalidation was performed. */ void iommu_gas_remove(struct iommu_domain *domain, iommu_gaddr_t start, iommu_gaddr_t size) { struct iommu_map_entry *r1, *r2; struct iommu_map_entries_tailq gc; iommu_gas_remove_init(domain, &gc, &r1, &r2); IOMMU_DOMAIN_LOCK(domain); iommu_gas_remove_locked(domain, start, size, &gc, &r1, &r2); IOMMU_DOMAIN_UNLOCK(domain); iommu_gas_remove_cleanup(domain, &gc, &r1, &r2); } int iommu_gas_map(struct iommu_domain *domain, const struct bus_dma_tag_common *common, iommu_gaddr_t size, int offset, u_int eflags, u_int flags, vm_page_t *ma, struct iommu_map_entry **res) { struct iommu_gas_match_args a; struct iommu_map_entry *entry; int error; KASSERT((flags & ~(IOMMU_MF_CANWAIT | IOMMU_MF_CANSPLIT)) == 0, ("invalid flags 0x%x", flags)); a.size = size; a.offset = offset; a.common = common; a.gas_flags = flags; entry = iommu_gas_alloc_entry(domain, (flags & IOMMU_MF_CANWAIT) != 0 ? IOMMU_PGF_WAITOK : 0); if (entry == NULL) return (ENOMEM); a.entry = entry; IOMMU_DOMAIN_LOCK(domain); error = iommu_gas_find_space(domain, &a); if (error == ENOMEM) { IOMMU_DOMAIN_UNLOCK(domain); iommu_gas_free_entry(entry); return (error); } #ifdef INVARIANTS if (iommu_check_free) iommu_gas_check_free(domain); #endif KASSERT(error == 0, ("unexpected error %d from iommu_gas_find_entry", error)); KASSERT(entry->end < domain->end, ("allocated GPA %jx, max GPA %jx", (uintmax_t)entry->end, (uintmax_t)domain->end)); entry->flags |= eflags; IOMMU_DOMAIN_UNLOCK(domain); - error = domain->ops->map(domain, entry->start, - entry->end - entry->start, ma, eflags, + error = domain->ops->map(domain, entry, ma, eflags, ((flags & IOMMU_MF_CANWAIT) != 0 ? IOMMU_PGF_WAITOK : 0)); if (error == ENOMEM) { iommu_domain_unload_entry(entry, true, (flags & IOMMU_MF_CANWAIT) != 0); return (error); } KASSERT(error == 0, ("unexpected error %d from domain_map_buf", error)); *res = entry; return (0); } int iommu_gas_map_region(struct iommu_domain *domain, struct iommu_map_entry *entry, u_int eflags, u_int flags, vm_page_t *ma) { iommu_gaddr_t start; int error; KASSERT(entry->domain == domain, ("mismatched domain %p entry %p entry->domain %p", domain, entry, entry->domain)); KASSERT(entry->flags == 0, ("used RMRR entry %p %p %x", domain, entry, entry->flags)); KASSERT((flags & ~(IOMMU_MF_CANWAIT | IOMMU_MF_RMRR)) == 0, ("invalid flags 0x%x", flags)); start = entry->start; IOMMU_DOMAIN_LOCK(domain); error = iommu_gas_alloc_region(domain, entry, flags); if (error != 0) { IOMMU_DOMAIN_UNLOCK(domain); return (error); } entry->flags |= eflags; IOMMU_DOMAIN_UNLOCK(domain); if (entry->end == entry->start) return (0); - error = domain->ops->map(domain, entry->start, - entry->end - entry->start, ma + OFF_TO_IDX(start - entry->start), - eflags, ((flags & IOMMU_MF_CANWAIT) != 0 ? IOMMU_PGF_WAITOK : 0)); + error = domain->ops->map(domain, entry, + ma + OFF_TO_IDX(start - entry->start), eflags, + ((flags & IOMMU_MF_CANWAIT) != 0 ? IOMMU_PGF_WAITOK : 0)); if (error == ENOMEM) { iommu_domain_unload_entry(entry, false, (flags & IOMMU_MF_CANWAIT) != 0); return (error); } KASSERT(error == 0, ("unexpected error %d from domain_map_buf", error)); return (0); } static int iommu_gas_reserve_region_locked(struct iommu_domain *domain, iommu_gaddr_t start, iommu_gaddr_t end, struct iommu_map_entry *entry) { int error; IOMMU_DOMAIN_ASSERT_LOCKED(domain); entry->start = start; entry->end = end; error = iommu_gas_alloc_region(domain, entry, IOMMU_MF_CANWAIT); if (error == 0) entry->flags |= IOMMU_MAP_ENTRY_UNMAPPED; return (error); } int iommu_gas_reserve_region(struct iommu_domain *domain, iommu_gaddr_t start, iommu_gaddr_t end, struct iommu_map_entry **entry0) { struct iommu_map_entry *entry; int error; entry = iommu_gas_alloc_entry(domain, IOMMU_PGF_WAITOK); IOMMU_DOMAIN_LOCK(domain); error = iommu_gas_reserve_region_locked(domain, start, end, entry); IOMMU_DOMAIN_UNLOCK(domain); if (error != 0) iommu_gas_free_entry(entry); else if (entry0 != NULL) *entry0 = entry; return (error); } /* * As in iommu_gas_reserve_region, reserve [start, end), but allow for existing * entries. */ int iommu_gas_reserve_region_extend(struct iommu_domain *domain, iommu_gaddr_t start, iommu_gaddr_t end) { struct iommu_map_entry *entry, *next, *prev, key = {}; iommu_gaddr_t entry_start, entry_end; int error; error = 0; entry = NULL; end = ummin(end, domain->end); while (start < end) { /* Preallocate an entry. */ if (entry == NULL) entry = iommu_gas_alloc_entry(domain, IOMMU_PGF_WAITOK); /* Calculate the free region from here to the next entry. */ key.start = key.end = start; IOMMU_DOMAIN_LOCK(domain); next = RB_NFIND(iommu_gas_entries_tree, &domain->rb_root, &key); KASSERT(next != NULL, ("domain %p with end %#jx has no entry " "after %#jx", domain, (uintmax_t)domain->end, (uintmax_t)start)); entry_end = ummin(end, next->start); prev = RB_PREV(iommu_gas_entries_tree, &domain->rb_root, next); if (prev != NULL) entry_start = ummax(start, prev->end); else entry_start = start; start = next->end; /* Reserve the region if non-empty. */ if (entry_start != entry_end) { error = iommu_gas_reserve_region_locked(domain, entry_start, entry_end, entry); if (error != 0) { IOMMU_DOMAIN_UNLOCK(domain); break; } entry = NULL; } IOMMU_DOMAIN_UNLOCK(domain); } /* Release a preallocated entry if it was not used. */ if (entry != NULL) iommu_gas_free_entry(entry); return (error); } void iommu_unmap_msi(struct iommu_ctx *ctx) { struct iommu_map_entry *entry; struct iommu_domain *domain; domain = ctx->domain; entry = domain->msi_entry; if (entry == NULL) return; - domain->ops->unmap(domain, entry->start, entry->end - - entry->start, IOMMU_PGF_WAITOK); + domain->ops->unmap(domain, entry, IOMMU_PGF_WAITOK); iommu_gas_free_space(entry); iommu_gas_free_entry(entry); domain->msi_entry = NULL; domain->msi_base = 0; domain->msi_phys = 0; } int iommu_map_msi(struct iommu_ctx *ctx, iommu_gaddr_t size, int offset, u_int eflags, u_int flags, vm_page_t *ma) { struct iommu_domain *domain; struct iommu_map_entry *entry; int error; error = 0; domain = ctx->domain; /* Check if there is already an MSI page allocated */ IOMMU_DOMAIN_LOCK(domain); entry = domain->msi_entry; IOMMU_DOMAIN_UNLOCK(domain); if (entry == NULL) { error = iommu_gas_map(domain, &ctx->tag->common, size, offset, eflags, flags, ma, &entry); IOMMU_DOMAIN_LOCK(domain); if (error == 0) { if (domain->msi_entry == NULL) { MPASS(domain->msi_base == 0); MPASS(domain->msi_phys == 0); domain->msi_entry = entry; domain->msi_base = entry->start; domain->msi_phys = VM_PAGE_TO_PHYS(ma[0]); } else { /* * We lost the race and already have an * MSI page allocated. Free the unneeded entry. */ iommu_gas_free_entry(entry); } } else if (domain->msi_entry != NULL) { /* * The allocation failed, but another succeeded. * Return success as there is a valid MSI page. */ error = 0; } IOMMU_DOMAIN_UNLOCK(domain); } return (error); } void iommu_translate_msi(struct iommu_domain *domain, uint64_t *addr) { *addr = (*addr - domain->msi_phys) + domain->msi_base; KASSERT(*addr >= domain->msi_entry->start, ("%s: Address is below the MSI entry start address (%jx < %jx)", __func__, (uintmax_t)*addr, (uintmax_t)domain->msi_entry->start)); KASSERT(*addr + sizeof(*addr) <= domain->msi_entry->end, ("%s: Address is above the MSI entry end address (%jx < %jx)", __func__, (uintmax_t)*addr, (uintmax_t)domain->msi_entry->end)); } SYSCTL_NODE(_hw, OID_AUTO, iommu, CTLFLAG_RW | CTLFLAG_MPSAFE, NULL, ""); #ifdef INVARIANTS SYSCTL_INT(_hw_iommu, OID_AUTO, check_free, CTLFLAG_RWTUN, &iommu_check_free, 0, "Check the GPA RBtree for free_down and free_after validity"); #endif #include "opt_ddb.h" #ifdef DDB #include static void iommu_debug_dump_gas(struct iommu_domain *domain) { struct iommu_map_entry *entry; db_printf("iommu_domain %p tree %p iommu %p fl %#x\n", domain, &domain->rb_root, domain->iommu, domain->flags); db_printf("iommu_domain %p tree %p\n", domain, &domain->rb_root); RB_FOREACH(entry, iommu_gas_entries_tree, &domain->rb_root) { db_printf( " e %p [%#jx %#jx] fl %#x first %#jx last %#jx free_down %#jx", entry, (uintmax_t)entry->start, (uintmax_t)entry->end, entry->flags, (uintmax_t)entry->first, (uintmax_t)entry->last, (uintmax_t)entry->free_down); if (entry == domain->start_gap) db_printf(" start_gap"); if (entry == domain->first_place) db_printf(" first_place"); if (entry == domain->last_place) db_printf(" last_place"); db_printf("\n"); } } DB_SHOW_COMMAND(iommu_domain, iommu_domain_show) { struct iommu_domain *domain; if (!have_addr) { db_printf("show iommu_domain addr\n"); return; } domain = (void *)addr; iommu_debug_dump_gas(domain); } #endif diff --git a/sys/x86/iommu/intel_ctx.c b/sys/x86/iommu/intel_ctx.c index 659d5c8a35e6..5af5ac7335b8 100644 --- a/sys/x86/iommu/intel_ctx.c +++ b/sys/x86/iommu/intel_ctx.c @@ -1,952 +1,952 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2013 The FreeBSD Foundation * * This software was developed by Konstantin Belousov * under sponsorship from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static MALLOC_DEFINE(M_DMAR_CTX, "dmar_ctx", "Intel DMAR Context"); static MALLOC_DEFINE(M_DMAR_DOMAIN, "dmar_dom", "Intel DMAR Domain"); static void dmar_unref_domain_locked(struct dmar_unit *dmar, struct dmar_domain *domain); static void dmar_domain_destroy(struct dmar_domain *domain); static void dmar_free_ctx_locked(struct dmar_unit *dmar, struct dmar_ctx *ctx); static void dmar_free_ctx(struct dmar_ctx *ctx); static void dmar_ensure_ctx_page(struct dmar_unit *dmar, int bus) { struct sf_buf *sf; dmar_root_entry_t *re; vm_page_t ctxm; /* * Allocated context page must be linked. */ ctxm = iommu_pgalloc(dmar->ctx_obj, 1 + bus, IOMMU_PGF_NOALLOC); if (ctxm != NULL) return; /* * Page not present, allocate and link. Note that other * thread might execute this sequence in parallel. This * should be safe, because the context entries written by both * threads are equal. */ TD_PREP_PINNED_ASSERT; ctxm = iommu_pgalloc(dmar->ctx_obj, 1 + bus, IOMMU_PGF_ZERO | IOMMU_PGF_WAITOK); re = iommu_map_pgtbl(dmar->ctx_obj, 0, IOMMU_PGF_NOALLOC, &sf); re += bus; dmar_pte_store(&re->r1, DMAR_ROOT_R1_P | (DMAR_ROOT_R1_CTP_MASK & VM_PAGE_TO_PHYS(ctxm))); dmar_flush_root_to_ram(dmar, re); iommu_unmap_pgtbl(sf); TD_PINNED_ASSERT; } static dmar_ctx_entry_t * dmar_map_ctx_entry(struct dmar_ctx *ctx, struct sf_buf **sfp) { struct dmar_unit *dmar; dmar_ctx_entry_t *ctxp; dmar = CTX2DMAR(ctx); ctxp = iommu_map_pgtbl(dmar->ctx_obj, 1 + PCI_RID2BUS(ctx->context.rid), IOMMU_PGF_NOALLOC | IOMMU_PGF_WAITOK, sfp); ctxp += ctx->context.rid & 0xff; return (ctxp); } static void ctx_id_entry_init_one(dmar_ctx_entry_t *ctxp, struct dmar_domain *domain, vm_page_t ctx_root) { /* * For update due to move, the store is not atomic. It is * possible that DMAR read upper doubleword, while low * doubleword is not yet updated. The domain id is stored in * the upper doubleword, while the table pointer in the lower. * * There is no good solution, for the same reason it is wrong * to clear P bit in the ctx entry for update. */ dmar_pte_store1(&ctxp->ctx2, DMAR_CTX2_DID(domain->domain) | domain->awlvl); if (ctx_root == NULL) { dmar_pte_store1(&ctxp->ctx1, DMAR_CTX1_T_PASS | DMAR_CTX1_P); } else { dmar_pte_store1(&ctxp->ctx1, DMAR_CTX1_T_UNTR | (DMAR_CTX1_ASR_MASK & VM_PAGE_TO_PHYS(ctx_root)) | DMAR_CTX1_P); } } static void ctx_id_entry_init(struct dmar_ctx *ctx, dmar_ctx_entry_t *ctxp, bool move, int busno) { struct dmar_unit *unit; struct dmar_domain *domain; vm_page_t ctx_root; int i; domain = CTX2DOM(ctx); unit = DOM2DMAR(domain); KASSERT(move || (ctxp->ctx1 == 0 && ctxp->ctx2 == 0), ("dmar%d: initialized ctx entry %d:%d:%d 0x%jx 0x%jx", unit->iommu.unit, busno, pci_get_slot(ctx->context.tag->owner), pci_get_function(ctx->context.tag->owner), ctxp->ctx1, ctxp->ctx2)); if ((domain->iodom.flags & IOMMU_DOMAIN_IDMAP) != 0 && (unit->hw_ecap & DMAR_ECAP_PT) != 0) { KASSERT(domain->pgtbl_obj == NULL, ("ctx %p non-null pgtbl_obj", ctx)); ctx_root = NULL; } else { ctx_root = iommu_pgalloc(domain->pgtbl_obj, 0, IOMMU_PGF_NOALLOC); } if (iommu_is_buswide_ctx(DMAR2IOMMU(unit), busno)) { MPASS(!move); for (i = 0; i <= PCI_BUSMAX; i++) { ctx_id_entry_init_one(&ctxp[i], domain, ctx_root); } } else { ctx_id_entry_init_one(ctxp, domain, ctx_root); } dmar_flush_ctx_to_ram(unit, ctxp); } static int dmar_flush_for_ctx_entry(struct dmar_unit *dmar, bool force) { int error; /* * If dmar declares Caching Mode as Set, follow 11.5 "Caching * Mode Consideration" and do the (global) invalidation of the * negative TLB entries. */ if ((dmar->hw_cap & DMAR_CAP_CM) == 0 && !force) return (0); if (dmar->qi_enabled) { dmar_qi_invalidate_ctx_glob_locked(dmar); if ((dmar->hw_ecap & DMAR_ECAP_DI) != 0 || force) dmar_qi_invalidate_iotlb_glob_locked(dmar); return (0); } error = dmar_inv_ctx_glob(dmar); if (error == 0 && ((dmar->hw_ecap & DMAR_ECAP_DI) != 0 || force)) error = dmar_inv_iotlb_glob(dmar); return (error); } static int domain_init_rmrr(struct dmar_domain *domain, device_t dev, int bus, int slot, int func, int dev_domain, int dev_busno, const void *dev_path, int dev_path_len) { struct iommu_map_entries_tailq rmrr_entries; struct iommu_map_entry *entry, *entry1; vm_page_t *ma; iommu_gaddr_t start, end; vm_pindex_t size, i; int error, error1; if (!dmar_rmrr_enable) return (0); error = 0; TAILQ_INIT(&rmrr_entries); dmar_dev_parse_rmrr(domain, dev_domain, dev_busno, dev_path, dev_path_len, &rmrr_entries); TAILQ_FOREACH_SAFE(entry, &rmrr_entries, dmamap_link, entry1) { /* * VT-d specification requires that the start of an * RMRR entry is 4k-aligned. Buggy BIOSes put * anything into the start and end fields. Truncate * and round as neccesary. * * We also allow the overlapping RMRR entries, see * iommu_gas_alloc_region(). */ start = entry->start; end = entry->end; if (bootverbose) printf("dmar%d ctx pci%d:%d:%d RMRR [%#jx, %#jx]\n", domain->iodom.iommu->unit, bus, slot, func, (uintmax_t)start, (uintmax_t)end); entry->start = trunc_page(start); entry->end = round_page(end); if (entry->start == entry->end) { /* Workaround for some AMI (?) BIOSes */ if (bootverbose) { if (dev != NULL) device_printf(dev, ""); printf("pci%d:%d:%d ", bus, slot, func); printf("BIOS bug: dmar%d RMRR " "region (%jx, %jx) corrected\n", domain->iodom.iommu->unit, start, end); } entry->end += IOMMU_PAGE_SIZE * 0x20; } size = OFF_TO_IDX(entry->end - entry->start); ma = malloc(sizeof(vm_page_t) * size, M_TEMP, M_WAITOK); for (i = 0; i < size; i++) { ma[i] = vm_page_getfake(entry->start + PAGE_SIZE * i, VM_MEMATTR_DEFAULT); } error1 = iommu_gas_map_region(DOM2IODOM(domain), entry, IOMMU_MAP_ENTRY_READ | IOMMU_MAP_ENTRY_WRITE, IOMMU_MF_CANWAIT | IOMMU_MF_RMRR, ma); /* * Non-failed RMRR entries are owned by context rb * tree. Get rid of the failed entry, but do not stop * the loop. Rest of the parsed RMRR entries are * loaded and removed on the context destruction. */ if (error1 == 0 && entry->end != entry->start) { IOMMU_LOCK(domain->iodom.iommu); domain->refs++; /* XXXKIB prevent free */ domain->iodom.flags |= IOMMU_DOMAIN_RMRR; IOMMU_UNLOCK(domain->iodom.iommu); } else { if (error1 != 0) { if (dev != NULL) device_printf(dev, ""); printf("pci%d:%d:%d ", bus, slot, func); printf( "dmar%d failed to map RMRR region (%jx, %jx) %d\n", domain->iodom.iommu->unit, start, end, error1); error = error1; } TAILQ_REMOVE(&rmrr_entries, entry, dmamap_link); iommu_gas_free_entry(entry); } for (i = 0; i < size; i++) vm_page_putfake(ma[i]); free(ma, M_TEMP); } return (error); } /* * PCI memory address space is shared between memory-mapped devices (MMIO) and * host memory (which may be remapped by an IOMMU). Device accesses to an * address within a memory aperture in a PCIe root port will be treated as * peer-to-peer and not forwarded to an IOMMU. To avoid this, reserve the * address space of the root port's memory apertures in the address space used * by the IOMMU for remapping. */ static int dmar_reserve_pci_regions(struct dmar_domain *domain, device_t dev) { struct iommu_domain *iodom; device_t root; uint32_t val; uint64_t base, limit; int error; iodom = DOM2IODOM(domain); root = pci_find_pcie_root_port(dev); if (root == NULL) return (0); /* Disable downstream memory */ base = PCI_PPBMEMBASE(0, pci_read_config(root, PCIR_MEMBASE_1, 2)); limit = PCI_PPBMEMLIMIT(0, pci_read_config(root, PCIR_MEMLIMIT_1, 2)); error = iommu_gas_reserve_region_extend(iodom, base, limit + 1); if (bootverbose || error != 0) device_printf(dev, "DMAR reserve [%#jx-%#jx] (error %d)\n", base, limit + 1, error); if (error != 0) return (error); /* Disable downstream prefetchable memory */ val = pci_read_config(root, PCIR_PMBASEL_1, 2); if (val != 0 || pci_read_config(root, PCIR_PMLIMITL_1, 2) != 0) { if ((val & PCIM_BRPM_MASK) == PCIM_BRPM_64) { base = PCI_PPBMEMBASE( pci_read_config(root, PCIR_PMBASEH_1, 4), val); limit = PCI_PPBMEMLIMIT( pci_read_config(root, PCIR_PMLIMITH_1, 4), pci_read_config(root, PCIR_PMLIMITL_1, 2)); } else { base = PCI_PPBMEMBASE(0, val); limit = PCI_PPBMEMLIMIT(0, pci_read_config(root, PCIR_PMLIMITL_1, 2)); } error = iommu_gas_reserve_region_extend(iodom, base, limit + 1); if (bootverbose || error != 0) device_printf(dev, "DMAR reserve [%#jx-%#jx] " "(error %d)\n", base, limit + 1, error); if (error != 0) return (error); } return (error); } static struct dmar_domain * dmar_domain_alloc(struct dmar_unit *dmar, bool id_mapped) { struct iommu_domain *iodom; struct iommu_unit *unit; struct dmar_domain *domain; int error, id, mgaw; id = alloc_unr(dmar->domids); if (id == -1) return (NULL); domain = malloc(sizeof(*domain), M_DMAR_DOMAIN, M_WAITOK | M_ZERO); iodom = DOM2IODOM(domain); unit = DMAR2IOMMU(dmar); domain->domain = id; LIST_INIT(&domain->contexts); iommu_domain_init(unit, iodom, &dmar_domain_map_ops); domain->dmar = dmar; /* * For now, use the maximal usable physical address of the * installed memory to calculate the mgaw on id_mapped domain. * It is useful for the identity mapping, and less so for the * virtualized bus address space. */ domain->iodom.end = id_mapped ? ptoa(Maxmem) : BUS_SPACE_MAXADDR; mgaw = dmar_maxaddr2mgaw(dmar, domain->iodom.end, !id_mapped); error = domain_set_agaw(domain, mgaw); if (error != 0) goto fail; if (!id_mapped) /* Use all supported address space for remapping. */ domain->iodom.end = 1ULL << (domain->agaw - 1); iommu_gas_init_domain(DOM2IODOM(domain)); if (id_mapped) { if ((dmar->hw_ecap & DMAR_ECAP_PT) == 0) { domain->pgtbl_obj = dmar_get_idmap_pgtbl(domain, domain->iodom.end); } domain->iodom.flags |= IOMMU_DOMAIN_IDMAP; } else { error = dmar_domain_alloc_pgtbl(domain); if (error != 0) goto fail; /* Disable local apic region access */ error = iommu_gas_reserve_region(iodom, 0xfee00000, 0xfeefffff + 1, &iodom->msi_entry); if (error != 0) goto fail; } return (domain); fail: dmar_domain_destroy(domain); return (NULL); } static struct dmar_ctx * dmar_ctx_alloc(struct dmar_domain *domain, uint16_t rid) { struct dmar_ctx *ctx; ctx = malloc(sizeof(*ctx), M_DMAR_CTX, M_WAITOK | M_ZERO); ctx->context.domain = DOM2IODOM(domain); ctx->context.tag = malloc(sizeof(struct bus_dma_tag_iommu), M_DMAR_CTX, M_WAITOK | M_ZERO); ctx->context.rid = rid; ctx->refs = 1; return (ctx); } static void dmar_ctx_link(struct dmar_ctx *ctx) { struct dmar_domain *domain; domain = CTX2DOM(ctx); IOMMU_ASSERT_LOCKED(domain->iodom.iommu); KASSERT(domain->refs >= domain->ctx_cnt, ("dom %p ref underflow %d %d", domain, domain->refs, domain->ctx_cnt)); domain->refs++; domain->ctx_cnt++; LIST_INSERT_HEAD(&domain->contexts, ctx, link); } static void dmar_ctx_unlink(struct dmar_ctx *ctx) { struct dmar_domain *domain; domain = CTX2DOM(ctx); IOMMU_ASSERT_LOCKED(domain->iodom.iommu); KASSERT(domain->refs > 0, ("domain %p ctx dtr refs %d", domain, domain->refs)); KASSERT(domain->ctx_cnt >= domain->refs, ("domain %p ctx dtr refs %d ctx_cnt %d", domain, domain->refs, domain->ctx_cnt)); domain->refs--; domain->ctx_cnt--; LIST_REMOVE(ctx, link); } static void dmar_domain_destroy(struct dmar_domain *domain) { struct iommu_domain *iodom; struct dmar_unit *dmar; iodom = DOM2IODOM(domain); KASSERT(TAILQ_EMPTY(&domain->iodom.unload_entries), ("unfinished unloads %p", domain)); KASSERT(LIST_EMPTY(&domain->contexts), ("destroying dom %p with contexts", domain)); KASSERT(domain->ctx_cnt == 0, ("destroying dom %p with ctx_cnt %d", domain, domain->ctx_cnt)); KASSERT(domain->refs == 0, ("destroying dom %p with refs %d", domain, domain->refs)); if ((domain->iodom.flags & IOMMU_DOMAIN_GAS_INITED) != 0) { DMAR_DOMAIN_LOCK(domain); iommu_gas_fini_domain(iodom); DMAR_DOMAIN_UNLOCK(domain); } if ((domain->iodom.flags & IOMMU_DOMAIN_PGTBL_INITED) != 0) { if (domain->pgtbl_obj != NULL) DMAR_DOMAIN_PGLOCK(domain); dmar_domain_free_pgtbl(domain); } iommu_domain_fini(iodom); dmar = DOM2DMAR(domain); free_unr(dmar->domids, domain->domain); free(domain, M_DMAR_DOMAIN); } static struct dmar_ctx * dmar_get_ctx_for_dev1(struct dmar_unit *dmar, device_t dev, uint16_t rid, int dev_domain, int dev_busno, const void *dev_path, int dev_path_len, bool id_mapped, bool rmrr_init) { struct dmar_domain *domain, *domain1; struct dmar_ctx *ctx, *ctx1; struct iommu_unit *unit __diagused; dmar_ctx_entry_t *ctxp; struct sf_buf *sf; int bus, slot, func, error; bool enable; if (dev != NULL) { bus = pci_get_bus(dev); slot = pci_get_slot(dev); func = pci_get_function(dev); } else { bus = PCI_RID2BUS(rid); slot = PCI_RID2SLOT(rid); func = PCI_RID2FUNC(rid); } enable = false; TD_PREP_PINNED_ASSERT; unit = DMAR2IOMMU(dmar); DMAR_LOCK(dmar); KASSERT(!iommu_is_buswide_ctx(unit, bus) || (slot == 0 && func == 0), ("iommu%d pci%d:%d:%d get_ctx for buswide", dmar->iommu.unit, bus, slot, func)); ctx = dmar_find_ctx_locked(dmar, rid); error = 0; if (ctx == NULL) { /* * Perform the allocations which require sleep or have * higher chance to succeed if the sleep is allowed. */ DMAR_UNLOCK(dmar); dmar_ensure_ctx_page(dmar, PCI_RID2BUS(rid)); domain1 = dmar_domain_alloc(dmar, id_mapped); if (domain1 == NULL) { TD_PINNED_ASSERT; return (NULL); } if (!id_mapped) { error = domain_init_rmrr(domain1, dev, bus, slot, func, dev_domain, dev_busno, dev_path, dev_path_len); if (error == 0 && dev != NULL) error = dmar_reserve_pci_regions(domain1, dev); if (error != 0) { dmar_domain_destroy(domain1); TD_PINNED_ASSERT; return (NULL); } } ctx1 = dmar_ctx_alloc(domain1, rid); ctxp = dmar_map_ctx_entry(ctx1, &sf); DMAR_LOCK(dmar); /* * Recheck the contexts, other thread might have * already allocated needed one. */ ctx = dmar_find_ctx_locked(dmar, rid); if (ctx == NULL) { domain = domain1; ctx = ctx1; dmar_ctx_link(ctx); ctx->context.tag->owner = dev; iommu_device_tag_init(CTX2IOCTX(ctx), dev); /* * This is the first activated context for the * DMAR unit. Enable the translation after * everything is set up. */ if (LIST_EMPTY(&dmar->domains)) enable = true; LIST_INSERT_HEAD(&dmar->domains, domain, link); ctx_id_entry_init(ctx, ctxp, false, bus); if (dev != NULL) { device_printf(dev, "dmar%d pci%d:%d:%d:%d rid %x domain %d mgaw %d " "agaw %d %s-mapped\n", dmar->iommu.unit, dmar->segment, bus, slot, func, rid, domain->domain, domain->mgaw, domain->agaw, id_mapped ? "id" : "re"); } iommu_unmap_pgtbl(sf); } else { iommu_unmap_pgtbl(sf); dmar_domain_destroy(domain1); /* Nothing needs to be done to destroy ctx1. */ free(ctx1, M_DMAR_CTX); domain = CTX2DOM(ctx); ctx->refs++; /* tag referenced us */ } } else { domain = CTX2DOM(ctx); if (ctx->context.tag->owner == NULL) ctx->context.tag->owner = dev; ctx->refs++; /* tag referenced us */ } error = dmar_flush_for_ctx_entry(dmar, enable); if (error != 0) { dmar_free_ctx_locked(dmar, ctx); TD_PINNED_ASSERT; return (NULL); } /* * The dmar lock was potentially dropped between check for the * empty context list and now. Recheck the state of GCMD_TE * to avoid unneeded command. */ if (enable && !rmrr_init && (dmar->hw_gcmd & DMAR_GCMD_TE) == 0) { error = dmar_disable_protected_regions(dmar); if (error != 0) printf("dmar%d: Failed to disable protected regions\n", dmar->iommu.unit); error = dmar_enable_translation(dmar); if (error == 0) { if (bootverbose) { printf("dmar%d: enabled translation\n", dmar->iommu.unit); } } else { printf("dmar%d: enabling translation failed, " "error %d\n", dmar->iommu.unit, error); dmar_free_ctx_locked(dmar, ctx); TD_PINNED_ASSERT; return (NULL); } } DMAR_UNLOCK(dmar); TD_PINNED_ASSERT; return (ctx); } struct dmar_ctx * dmar_get_ctx_for_dev(struct dmar_unit *dmar, device_t dev, uint16_t rid, bool id_mapped, bool rmrr_init) { int dev_domain, dev_path_len, dev_busno; dev_domain = pci_get_domain(dev); dev_path_len = dmar_dev_depth(dev); ACPI_DMAR_PCI_PATH dev_path[dev_path_len]; dmar_dev_path(dev, &dev_busno, dev_path, dev_path_len); return (dmar_get_ctx_for_dev1(dmar, dev, rid, dev_domain, dev_busno, dev_path, dev_path_len, id_mapped, rmrr_init)); } struct dmar_ctx * dmar_get_ctx_for_devpath(struct dmar_unit *dmar, uint16_t rid, int dev_domain, int dev_busno, const void *dev_path, int dev_path_len, bool id_mapped, bool rmrr_init) { return (dmar_get_ctx_for_dev1(dmar, NULL, rid, dev_domain, dev_busno, dev_path, dev_path_len, id_mapped, rmrr_init)); } int dmar_move_ctx_to_domain(struct dmar_domain *domain, struct dmar_ctx *ctx) { struct dmar_unit *dmar; struct dmar_domain *old_domain; dmar_ctx_entry_t *ctxp; struct sf_buf *sf; int error; dmar = domain->dmar; old_domain = CTX2DOM(ctx); if (domain == old_domain) return (0); KASSERT(old_domain->iodom.iommu == domain->iodom.iommu, ("domain %p %u moving between dmars %u %u", domain, domain->domain, old_domain->iodom.iommu->unit, domain->iodom.iommu->unit)); TD_PREP_PINNED_ASSERT; ctxp = dmar_map_ctx_entry(ctx, &sf); DMAR_LOCK(dmar); dmar_ctx_unlink(ctx); ctx->context.domain = &domain->iodom; dmar_ctx_link(ctx); ctx_id_entry_init(ctx, ctxp, true, PCI_BUSMAX + 100); iommu_unmap_pgtbl(sf); error = dmar_flush_for_ctx_entry(dmar, true); /* If flush failed, rolling back would not work as well. */ printf("dmar%d rid %x domain %d->%d %s-mapped\n", dmar->iommu.unit, ctx->context.rid, old_domain->domain, domain->domain, (domain->iodom.flags & IOMMU_DOMAIN_IDMAP) != 0 ? "id" : "re"); dmar_unref_domain_locked(dmar, old_domain); TD_PINNED_ASSERT; return (error); } static void dmar_unref_domain_locked(struct dmar_unit *dmar, struct dmar_domain *domain) { DMAR_ASSERT_LOCKED(dmar); KASSERT(domain->refs >= 1, ("dmar %d domain %p refs %u", dmar->iommu.unit, domain, domain->refs)); KASSERT(domain->refs > domain->ctx_cnt, ("dmar %d domain %p refs %d ctx_cnt %d", dmar->iommu.unit, domain, domain->refs, domain->ctx_cnt)); if (domain->refs > 1) { domain->refs--; DMAR_UNLOCK(dmar); return; } KASSERT((domain->iodom.flags & IOMMU_DOMAIN_RMRR) == 0, ("lost ref on RMRR domain %p", domain)); LIST_REMOVE(domain, link); DMAR_UNLOCK(dmar); taskqueue_drain(dmar->iommu.delayed_taskqueue, &domain->iodom.unload_task); dmar_domain_destroy(domain); } static void dmar_free_ctx_locked(struct dmar_unit *dmar, struct dmar_ctx *ctx) { struct sf_buf *sf; dmar_ctx_entry_t *ctxp; struct dmar_domain *domain; DMAR_ASSERT_LOCKED(dmar); KASSERT(ctx->refs >= 1, ("dmar %p ctx %p refs %u", dmar, ctx, ctx->refs)); /* * If our reference is not last, only the dereference should * be performed. */ if (ctx->refs > 1) { ctx->refs--; DMAR_UNLOCK(dmar); return; } KASSERT((ctx->context.flags & IOMMU_CTX_DISABLED) == 0, ("lost ref on disabled ctx %p", ctx)); /* * Otherwise, the context entry must be cleared before the * page table is destroyed. The mapping of the context * entries page could require sleep, unlock the dmar. */ DMAR_UNLOCK(dmar); TD_PREP_PINNED_ASSERT; ctxp = dmar_map_ctx_entry(ctx, &sf); DMAR_LOCK(dmar); KASSERT(ctx->refs >= 1, ("dmar %p ctx %p refs %u", dmar, ctx, ctx->refs)); /* * Other thread might have referenced the context, in which * case again only the dereference should be performed. */ if (ctx->refs > 1) { ctx->refs--; DMAR_UNLOCK(dmar); iommu_unmap_pgtbl(sf); TD_PINNED_ASSERT; return; } KASSERT((ctx->context.flags & IOMMU_CTX_DISABLED) == 0, ("lost ref on disabled ctx %p", ctx)); /* * Clear the context pointer and flush the caches. * XXXKIB: cannot do this if any RMRR entries are still present. */ dmar_pte_clear(&ctxp->ctx1); ctxp->ctx2 = 0; dmar_flush_ctx_to_ram(dmar, ctxp); dmar_inv_ctx_glob(dmar); if ((dmar->hw_ecap & DMAR_ECAP_DI) != 0) { if (dmar->qi_enabled) dmar_qi_invalidate_iotlb_glob_locked(dmar); else dmar_inv_iotlb_glob(dmar); } iommu_unmap_pgtbl(sf); domain = CTX2DOM(ctx); dmar_ctx_unlink(ctx); free(ctx->context.tag, M_DMAR_CTX); free(ctx, M_DMAR_CTX); dmar_unref_domain_locked(dmar, domain); TD_PINNED_ASSERT; } static void dmar_free_ctx(struct dmar_ctx *ctx) { struct dmar_unit *dmar; dmar = CTX2DMAR(ctx); DMAR_LOCK(dmar); dmar_free_ctx_locked(dmar, ctx); } /* * Returns with the domain locked. */ struct dmar_ctx * dmar_find_ctx_locked(struct dmar_unit *dmar, uint16_t rid) { struct dmar_domain *domain; struct dmar_ctx *ctx; DMAR_ASSERT_LOCKED(dmar); LIST_FOREACH(domain, &dmar->domains, link) { LIST_FOREACH(ctx, &domain->contexts, link) { if (ctx->context.rid == rid) return (ctx); } } return (NULL); } /* * If the given value for "free" is true, then the caller must not be using * the entry's dmamap_link field. */ void dmar_domain_unload_entry(struct iommu_map_entry *entry, bool free, bool cansleep) { struct dmar_domain *domain; struct dmar_unit *unit; domain = IODOM2DOM(entry->domain); unit = DOM2DMAR(domain); /* * If "free" is false, then the IOTLB invalidation must be performed * synchronously. Otherwise, the caller might free the entry before * dmar_qi_task() is finished processing it. */ if (unit->qi_enabled) { if (free) { DMAR_LOCK(unit); iommu_qi_invalidate_locked(&domain->iodom, entry, true); DMAR_UNLOCK(unit); } else { iommu_qi_invalidate_sync(&domain->iodom, entry->start, entry->end - entry->start, cansleep); iommu_domain_free_entry(entry, false); } } else { dmar_flush_iotlb_sync(domain, entry->start, entry->end - entry->start); iommu_domain_free_entry(entry, free); } } static bool dmar_domain_unload_emit_wait(struct dmar_domain *domain, struct iommu_map_entry *entry) { if (TAILQ_NEXT(entry, dmamap_link) == NULL) return (true); return (domain->batch_no++ % iommu_qi_batch_coalesce == 0); } void dmar_domain_unload(struct iommu_domain *iodom, struct iommu_map_entries_tailq *entries, bool cansleep) { struct dmar_domain *domain; struct dmar_unit *unit; struct iommu_map_entry *entry, *entry1; int error __diagused; domain = IODOM2DOM(iodom); unit = DOM2DMAR(domain); TAILQ_FOREACH_SAFE(entry, entries, dmamap_link, entry1) { KASSERT((entry->flags & IOMMU_MAP_ENTRY_MAP) != 0, ("not mapped entry %p %p", domain, entry)); - error = iodom->ops->unmap(iodom, entry->start, entry->end - - entry->start, cansleep ? IOMMU_PGF_WAITOK : 0); + error = iodom->ops->unmap(iodom, entry, + cansleep ? IOMMU_PGF_WAITOK : 0); KASSERT(error == 0, ("unmap %p error %d", domain, error)); if (!unit->qi_enabled) { dmar_flush_iotlb_sync(domain, entry->start, entry->end - entry->start); TAILQ_REMOVE(entries, entry, dmamap_link); iommu_domain_free_entry(entry, true); } } if (TAILQ_EMPTY(entries)) return; KASSERT(unit->qi_enabled, ("loaded entry left")); DMAR_LOCK(unit); while ((entry = TAILQ_FIRST(entries)) != NULL) { TAILQ_REMOVE(entries, entry, dmamap_link); iommu_qi_invalidate_locked(&domain->iodom, entry, dmar_domain_unload_emit_wait(domain, entry)); } DMAR_UNLOCK(unit); } struct iommu_ctx * dmar_get_ctx(struct iommu_unit *iommu, device_t dev, uint16_t rid, bool id_mapped, bool rmrr_init) { struct dmar_unit *dmar; struct dmar_ctx *ret; dmar = IOMMU2DMAR(iommu); ret = dmar_get_ctx_for_dev(dmar, dev, rid, id_mapped, rmrr_init); return (CTX2IOCTX(ret)); } void dmar_free_ctx_locked_method(struct iommu_unit *iommu, struct iommu_ctx *context) { struct dmar_unit *dmar; struct dmar_ctx *ctx; dmar = IOMMU2DMAR(iommu); ctx = IOCTX2CTX(context); dmar_free_ctx_locked(dmar, ctx); } void dmar_free_ctx_method(struct iommu_ctx *context) { struct dmar_ctx *ctx; ctx = IOCTX2CTX(context); dmar_free_ctx(ctx); } diff --git a/sys/x86/iommu/intel_idpgtbl.c b/sys/x86/iommu/intel_idpgtbl.c index 7def178146d1..ffb4c73a6c5a 100644 --- a/sys/x86/iommu/intel_idpgtbl.c +++ b/sys/x86/iommu/intel_idpgtbl.c @@ -1,783 +1,788 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2013 The FreeBSD Foundation * * This software was developed by Konstantin Belousov * under sponsorship from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static int dmar_unmap_buf_locked(struct dmar_domain *domain, iommu_gaddr_t base, iommu_gaddr_t size, int flags); /* * The cache of the identity mapping page tables for the DMARs. Using * the cache saves significant amount of memory for page tables by * reusing the page tables, since usually DMARs are identical and have * the same capabilities. Still, cache records the information needed * to match DMAR capabilities and page table format, to correctly * handle different DMARs. */ struct idpgtbl { iommu_gaddr_t maxaddr; /* Page table covers the guest address range [0..maxaddr) */ int pglvl; /* Total page table levels ignoring superpages */ int leaf; /* The last materialized page table level, it is non-zero if superpages are supported */ vm_object_t pgtbl_obj; /* The page table pages */ LIST_ENTRY(idpgtbl) link; }; static struct sx idpgtbl_lock; SX_SYSINIT(idpgtbl, &idpgtbl_lock, "idpgtbl"); static LIST_HEAD(, idpgtbl) idpgtbls = LIST_HEAD_INITIALIZER(idpgtbls); static MALLOC_DEFINE(M_DMAR_IDPGTBL, "dmar_idpgtbl", "Intel DMAR Identity mappings cache elements"); /* * Build the next level of the page tables for the identity mapping. * - lvl is the level to build; * - idx is the index of the page table page in the pgtbl_obj, which is * being allocated filled now; * - addr is the starting address in the bus address space which is * mapped by the page table page. */ static void dmar_idmap_nextlvl(struct idpgtbl *tbl, int lvl, vm_pindex_t idx, iommu_gaddr_t addr) { vm_page_t m1; iommu_pte_t *pte; struct sf_buf *sf; iommu_gaddr_t f, pg_sz; vm_pindex_t base; int i; VM_OBJECT_ASSERT_LOCKED(tbl->pgtbl_obj); if (addr >= tbl->maxaddr) return; (void)iommu_pgalloc(tbl->pgtbl_obj, idx, IOMMU_PGF_OBJL | IOMMU_PGF_WAITOK | IOMMU_PGF_ZERO); base = idx * IOMMU_NPTEPG + 1; /* Index of the first child page of idx */ pg_sz = pglvl_page_size(tbl->pglvl, lvl); if (lvl != tbl->leaf) { for (i = 0, f = addr; i < IOMMU_NPTEPG; i++, f += pg_sz) dmar_idmap_nextlvl(tbl, lvl + 1, base + i, f); } VM_OBJECT_WUNLOCK(tbl->pgtbl_obj); pte = iommu_map_pgtbl(tbl->pgtbl_obj, idx, IOMMU_PGF_WAITOK, &sf); if (lvl == tbl->leaf) { for (i = 0, f = addr; i < IOMMU_NPTEPG; i++, f += pg_sz) { if (f >= tbl->maxaddr) break; pte[i].pte = (DMAR_PTE_ADDR_MASK & f) | DMAR_PTE_R | DMAR_PTE_W; } } else { for (i = 0, f = addr; i < IOMMU_NPTEPG; i++, f += pg_sz) { if (f >= tbl->maxaddr) break; m1 = iommu_pgalloc(tbl->pgtbl_obj, base + i, IOMMU_PGF_NOALLOC); KASSERT(m1 != NULL, ("lost page table page")); pte[i].pte = (DMAR_PTE_ADDR_MASK & VM_PAGE_TO_PHYS(m1)) | DMAR_PTE_R | DMAR_PTE_W; } } /* dmar_get_idmap_pgtbl flushes CPU cache if needed. */ iommu_unmap_pgtbl(sf); VM_OBJECT_WLOCK(tbl->pgtbl_obj); } /* * Find a ready and compatible identity-mapping page table in the * cache. If not found, populate the identity-mapping page table for * the context, up to the maxaddr. The maxaddr byte is allowed to be * not mapped, which is aligned with the definition of Maxmem as the * highest usable physical address + 1. If superpages are used, the * maxaddr is typically mapped. */ vm_object_t dmar_get_idmap_pgtbl(struct dmar_domain *domain, iommu_gaddr_t maxaddr) { struct dmar_unit *unit; struct idpgtbl *tbl; vm_object_t res; vm_page_t m; int leaf, i; leaf = 0; /* silence gcc */ /* * First, determine where to stop the paging structures. */ for (i = 0; i < domain->pglvl; i++) { if (i == domain->pglvl - 1 || domain_is_sp_lvl(domain, i)) { leaf = i; break; } } /* * Search the cache for a compatible page table. Qualified * page table must map up to maxaddr, its level must be * supported by the DMAR and leaf should be equal to the * calculated value. The later restriction could be lifted * but I believe it is currently impossible to have any * deviations for existing hardware. */ sx_slock(&idpgtbl_lock); LIST_FOREACH(tbl, &idpgtbls, link) { if (tbl->maxaddr >= maxaddr && dmar_pglvl_supported(domain->dmar, tbl->pglvl) && tbl->leaf == leaf) { res = tbl->pgtbl_obj; vm_object_reference(res); sx_sunlock(&idpgtbl_lock); domain->pglvl = tbl->pglvl; /* XXXKIB ? */ goto end; } } /* * Not found in cache, relock the cache into exclusive mode to * be able to add element, and recheck cache again after the * relock. */ sx_sunlock(&idpgtbl_lock); sx_xlock(&idpgtbl_lock); LIST_FOREACH(tbl, &idpgtbls, link) { if (tbl->maxaddr >= maxaddr && dmar_pglvl_supported(domain->dmar, tbl->pglvl) && tbl->leaf == leaf) { res = tbl->pgtbl_obj; vm_object_reference(res); sx_xunlock(&idpgtbl_lock); domain->pglvl = tbl->pglvl; /* XXXKIB ? */ return (res); } } /* * Still not found, create new page table. */ tbl = malloc(sizeof(*tbl), M_DMAR_IDPGTBL, M_WAITOK); tbl->pglvl = domain->pglvl; tbl->leaf = leaf; tbl->maxaddr = maxaddr; tbl->pgtbl_obj = vm_pager_allocate(OBJT_PHYS, NULL, IDX_TO_OFF(pglvl_max_pages(tbl->pglvl)), 0, 0, NULL); VM_OBJECT_WLOCK(tbl->pgtbl_obj); dmar_idmap_nextlvl(tbl, 0, 0, 0); VM_OBJECT_WUNLOCK(tbl->pgtbl_obj); LIST_INSERT_HEAD(&idpgtbls, tbl, link); res = tbl->pgtbl_obj; vm_object_reference(res); sx_xunlock(&idpgtbl_lock); end: /* * Table was found or created. * * If DMAR does not snoop paging structures accesses, flush * CPU cache to memory. Note that dmar_unmap_pgtbl() coherent * argument was possibly invalid at the time of the identity * page table creation, since DMAR which was passed at the * time of creation could be coherent, while current DMAR is * not. * * If DMAR cannot look into the chipset write buffer, flush it * as well. */ unit = domain->dmar; if (!DMAR_IS_COHERENT(unit)) { VM_OBJECT_WLOCK(res); for (m = vm_page_lookup(res, 0); m != NULL; m = vm_page_next(m)) pmap_invalidate_cache_pages(&m, 1); VM_OBJECT_WUNLOCK(res); } if ((unit->hw_cap & DMAR_CAP_RWBF) != 0) { DMAR_LOCK(unit); dmar_flush_write_bufs(unit); DMAR_UNLOCK(unit); } return (res); } /* * Return a reference to the identity mapping page table to the cache. */ void dmar_put_idmap_pgtbl(vm_object_t obj) { struct idpgtbl *tbl, *tbl1; vm_object_t rmobj; sx_slock(&idpgtbl_lock); KASSERT(obj->ref_count >= 2, ("lost cache reference")); vm_object_deallocate(obj); /* * Cache always owns one last reference on the page table object. * If there is an additional reference, object must stay. */ if (obj->ref_count > 1) { sx_sunlock(&idpgtbl_lock); return; } /* * Cache reference is the last, remove cache element and free * page table object, returning the page table pages to the * system. */ sx_sunlock(&idpgtbl_lock); sx_xlock(&idpgtbl_lock); LIST_FOREACH_SAFE(tbl, &idpgtbls, link, tbl1) { rmobj = tbl->pgtbl_obj; if (rmobj->ref_count == 1) { LIST_REMOVE(tbl, link); atomic_subtract_int(&iommu_tbl_pagecnt, rmobj->resident_page_count); vm_object_deallocate(rmobj); free(tbl, M_DMAR_IDPGTBL); } } sx_xunlock(&idpgtbl_lock); } /* * The core routines to map and unmap host pages at the given guest * address. Support superpages. */ static iommu_pte_t * dmar_pgtbl_map_pte(struct dmar_domain *domain, iommu_gaddr_t base, int lvl, int flags, vm_pindex_t *idxp, struct sf_buf **sf) { vm_page_t m; struct sf_buf *sfp; iommu_pte_t *pte, *ptep; vm_pindex_t idx, idx1; DMAR_DOMAIN_ASSERT_PGLOCKED(domain); KASSERT((flags & IOMMU_PGF_OBJL) != 0, ("lost PGF_OBJL")); idx = pglvl_pgtbl_get_pindex(domain->pglvl, base, lvl); if (*sf != NULL && idx == *idxp) { pte = (iommu_pte_t *)sf_buf_kva(*sf); } else { if (*sf != NULL) iommu_unmap_pgtbl(*sf); *idxp = idx; retry: pte = iommu_map_pgtbl(domain->pgtbl_obj, idx, flags, sf); if (pte == NULL) { KASSERT(lvl > 0, ("lost root page table page %p", domain)); /* * Page table page does not exist, allocate * it and create a pte in the preceeding page level * to reference the allocated page table page. */ m = iommu_pgalloc(domain->pgtbl_obj, idx, flags | IOMMU_PGF_ZERO); if (m == NULL) return (NULL); /* * Prevent potential free while pgtbl_obj is * unlocked in the recursive call to * domain_pgtbl_map_pte(), if other thread did * pte write and clean while the lock is * dropped. */ vm_page_wire(m); sfp = NULL; ptep = dmar_pgtbl_map_pte(domain, base, lvl - 1, flags, &idx1, &sfp); if (ptep == NULL) { KASSERT(m->pindex != 0, ("loosing root page %p", domain)); vm_page_unwire_noq(m); iommu_pgfree(domain->pgtbl_obj, m->pindex, flags); return (NULL); } dmar_pte_store(&ptep->pte, DMAR_PTE_R | DMAR_PTE_W | VM_PAGE_TO_PHYS(m)); dmar_flush_pte_to_ram(domain->dmar, ptep); vm_page_wire(sf_buf_page(sfp)); vm_page_unwire_noq(m); iommu_unmap_pgtbl(sfp); /* Only executed once. */ goto retry; } } pte += pglvl_pgtbl_pte_off(domain->pglvl, base, lvl); return (pte); } static int dmar_map_buf_locked(struct dmar_domain *domain, iommu_gaddr_t base, iommu_gaddr_t size, vm_page_t *ma, uint64_t pflags, int flags) { iommu_pte_t *pte; struct sf_buf *sf; iommu_gaddr_t pg_sz, base1; vm_pindex_t pi, c, idx, run_sz; int lvl; bool superpage; DMAR_DOMAIN_ASSERT_PGLOCKED(domain); base1 = base; flags |= IOMMU_PGF_OBJL; TD_PREP_PINNED_ASSERT; for (sf = NULL, pi = 0; size > 0; base += pg_sz, size -= pg_sz, pi += run_sz) { for (lvl = 0, c = 0, superpage = false;; lvl++) { pg_sz = domain_page_size(domain, lvl); run_sz = pg_sz >> IOMMU_PAGE_SHIFT; if (lvl == domain->pglvl - 1) break; /* * Check if the current base suitable for the * superpage mapping. First, verify the level. */ if (!domain_is_sp_lvl(domain, lvl)) continue; /* * Next, look at the size of the mapping and * alignment of both guest and host addresses. */ if (size < pg_sz || (base & (pg_sz - 1)) != 0 || (VM_PAGE_TO_PHYS(ma[pi]) & (pg_sz - 1)) != 0) continue; /* All passed, check host pages contiguouty. */ if (c == 0) { for (c = 1; c < run_sz; c++) { if (VM_PAGE_TO_PHYS(ma[pi + c]) != VM_PAGE_TO_PHYS(ma[pi + c - 1]) + PAGE_SIZE) break; } } if (c >= run_sz) { superpage = true; break; } } KASSERT(size >= pg_sz, ("mapping loop overflow %p %jx %jx %jx", domain, (uintmax_t)base, (uintmax_t)size, (uintmax_t)pg_sz)); KASSERT(pg_sz > 0, ("pg_sz 0 lvl %d", lvl)); pte = dmar_pgtbl_map_pte(domain, base, lvl, flags, &idx, &sf); if (pte == NULL) { KASSERT((flags & IOMMU_PGF_WAITOK) == 0, ("failed waitable pte alloc %p", domain)); if (sf != NULL) iommu_unmap_pgtbl(sf); dmar_unmap_buf_locked(domain, base1, base - base1, flags); TD_PINNED_ASSERT; return (ENOMEM); } dmar_pte_store(&pte->pte, VM_PAGE_TO_PHYS(ma[pi]) | pflags | (superpage ? DMAR_PTE_SP : 0)); dmar_flush_pte_to_ram(domain->dmar, pte); vm_page_wire(sf_buf_page(sf)); } if (sf != NULL) iommu_unmap_pgtbl(sf); TD_PINNED_ASSERT; return (0); } static int -dmar_map_buf(struct iommu_domain *iodom, iommu_gaddr_t base, - iommu_gaddr_t size, vm_page_t *ma, uint64_t eflags, int flags) +dmar_map_buf(struct iommu_domain *iodom, struct iommu_map_entry *entry, + vm_page_t *ma, uint64_t eflags, int flags) { struct dmar_domain *domain; struct dmar_unit *unit; + iommu_gaddr_t base, size; uint64_t pflags; int error; + base = entry->start; + size = entry->end - entry->start; + pflags = ((eflags & IOMMU_MAP_ENTRY_READ) != 0 ? DMAR_PTE_R : 0) | ((eflags & IOMMU_MAP_ENTRY_WRITE) != 0 ? DMAR_PTE_W : 0) | ((eflags & IOMMU_MAP_ENTRY_SNOOP) != 0 ? DMAR_PTE_SNP : 0) | ((eflags & IOMMU_MAP_ENTRY_TM) != 0 ? DMAR_PTE_TM : 0); domain = IODOM2DOM(iodom); unit = domain->dmar; KASSERT((iodom->flags & IOMMU_DOMAIN_IDMAP) == 0, ("modifying idmap pagetable domain %p", domain)); KASSERT((base & IOMMU_PAGE_MASK) == 0, ("non-aligned base %p %jx %jx", domain, (uintmax_t)base, (uintmax_t)size)); KASSERT((size & IOMMU_PAGE_MASK) == 0, ("non-aligned size %p %jx %jx", domain, (uintmax_t)base, (uintmax_t)size)); KASSERT(size > 0, ("zero size %p %jx %jx", domain, (uintmax_t)base, (uintmax_t)size)); KASSERT(base < (1ULL << domain->agaw), ("base too high %p %jx %jx agaw %d", domain, (uintmax_t)base, (uintmax_t)size, domain->agaw)); KASSERT(base + size < (1ULL << domain->agaw), ("end too high %p %jx %jx agaw %d", domain, (uintmax_t)base, (uintmax_t)size, domain->agaw)); KASSERT(base + size > base, ("size overflow %p %jx %jx", domain, (uintmax_t)base, (uintmax_t)size)); KASSERT((pflags & (DMAR_PTE_R | DMAR_PTE_W)) != 0, ("neither read nor write %jx", (uintmax_t)pflags)); KASSERT((pflags & ~(DMAR_PTE_R | DMAR_PTE_W | DMAR_PTE_SNP | DMAR_PTE_TM)) == 0, ("invalid pte flags %jx", (uintmax_t)pflags)); KASSERT((pflags & DMAR_PTE_SNP) == 0 || (unit->hw_ecap & DMAR_ECAP_SC) != 0, ("PTE_SNP for dmar without snoop control %p %jx", domain, (uintmax_t)pflags)); KASSERT((pflags & DMAR_PTE_TM) == 0 || (unit->hw_ecap & DMAR_ECAP_DI) != 0, ("PTE_TM for dmar without DIOTLB %p %jx", domain, (uintmax_t)pflags)); KASSERT((flags & ~IOMMU_PGF_WAITOK) == 0, ("invalid flags %x", flags)); DMAR_DOMAIN_PGLOCK(domain); error = dmar_map_buf_locked(domain, base, size, ma, pflags, flags); DMAR_DOMAIN_PGUNLOCK(domain); if (error != 0) return (error); if ((unit->hw_cap & DMAR_CAP_CM) != 0) dmar_flush_iotlb_sync(domain, base, size); else if ((unit->hw_cap & DMAR_CAP_RWBF) != 0) { /* See 11.1 Write Buffer Flushing. */ DMAR_LOCK(unit); dmar_flush_write_bufs(unit); DMAR_UNLOCK(unit); } return (0); } static void dmar_unmap_clear_pte(struct dmar_domain *domain, iommu_gaddr_t base, int lvl, int flags, iommu_pte_t *pte, struct sf_buf **sf, bool free_fs); static void dmar_free_pgtbl_pde(struct dmar_domain *domain, iommu_gaddr_t base, int lvl, int flags) { struct sf_buf *sf; iommu_pte_t *pde; vm_pindex_t idx; sf = NULL; pde = dmar_pgtbl_map_pte(domain, base, lvl, flags, &idx, &sf); dmar_unmap_clear_pte(domain, base, lvl, flags, pde, &sf, true); } static void dmar_unmap_clear_pte(struct dmar_domain *domain, iommu_gaddr_t base, int lvl, int flags, iommu_pte_t *pte, struct sf_buf **sf, bool free_sf) { vm_page_t m; dmar_pte_clear(&pte->pte); dmar_flush_pte_to_ram(domain->dmar, pte); m = sf_buf_page(*sf); if (free_sf) { iommu_unmap_pgtbl(*sf); *sf = NULL; } if (!vm_page_unwire_noq(m)) return; KASSERT(lvl != 0, ("lost reference (lvl) on root pg domain %p base %jx lvl %d", domain, (uintmax_t)base, lvl)); KASSERT(m->pindex != 0, ("lost reference (idx) on root pg domain %p base %jx lvl %d", domain, (uintmax_t)base, lvl)); iommu_pgfree(domain->pgtbl_obj, m->pindex, flags); dmar_free_pgtbl_pde(domain, base, lvl - 1, flags); } /* * Assumes that the unmap is never partial. */ static int dmar_unmap_buf_locked(struct dmar_domain *domain, iommu_gaddr_t base, iommu_gaddr_t size, int flags) { iommu_pte_t *pte; struct sf_buf *sf; vm_pindex_t idx; iommu_gaddr_t pg_sz; int lvl; DMAR_DOMAIN_ASSERT_PGLOCKED(domain); if (size == 0) return (0); KASSERT((domain->iodom.flags & IOMMU_DOMAIN_IDMAP) == 0, ("modifying idmap pagetable domain %p", domain)); KASSERT((base & IOMMU_PAGE_MASK) == 0, ("non-aligned base %p %jx %jx", domain, (uintmax_t)base, (uintmax_t)size)); KASSERT((size & IOMMU_PAGE_MASK) == 0, ("non-aligned size %p %jx %jx", domain, (uintmax_t)base, (uintmax_t)size)); KASSERT(base < (1ULL << domain->agaw), ("base too high %p %jx %jx agaw %d", domain, (uintmax_t)base, (uintmax_t)size, domain->agaw)); KASSERT(base + size < (1ULL << domain->agaw), ("end too high %p %jx %jx agaw %d", domain, (uintmax_t)base, (uintmax_t)size, domain->agaw)); KASSERT(base + size > base, ("size overflow %p %jx %jx", domain, (uintmax_t)base, (uintmax_t)size)); KASSERT((flags & ~IOMMU_PGF_WAITOK) == 0, ("invalid flags %x", flags)); pg_sz = 0; /* silence gcc */ flags |= IOMMU_PGF_OBJL; TD_PREP_PINNED_ASSERT; for (sf = NULL; size > 0; base += pg_sz, size -= pg_sz) { for (lvl = 0; lvl < domain->pglvl; lvl++) { if (lvl != domain->pglvl - 1 && !domain_is_sp_lvl(domain, lvl)) continue; pg_sz = domain_page_size(domain, lvl); if (pg_sz > size) continue; pte = dmar_pgtbl_map_pte(domain, base, lvl, flags, &idx, &sf); KASSERT(pte != NULL, ("sleeping or page missed %p %jx %d 0x%x", domain, (uintmax_t)base, lvl, flags)); if ((pte->pte & DMAR_PTE_SP) != 0 || lvl == domain->pglvl - 1) { dmar_unmap_clear_pte(domain, base, lvl, flags, pte, &sf, false); break; } } KASSERT(size >= pg_sz, ("unmapping loop overflow %p %jx %jx %jx", domain, (uintmax_t)base, (uintmax_t)size, (uintmax_t)pg_sz)); } if (sf != NULL) iommu_unmap_pgtbl(sf); /* * See 11.1 Write Buffer Flushing for an explanation why RWBF * can be ignored there. */ TD_PINNED_ASSERT; return (0); } static int -dmar_unmap_buf(struct iommu_domain *iodom, iommu_gaddr_t base, - iommu_gaddr_t size, int flags) +dmar_unmap_buf(struct iommu_domain *iodom, struct iommu_map_entry *entry, + int flags) { struct dmar_domain *domain; int error; domain = IODOM2DOM(iodom); DMAR_DOMAIN_PGLOCK(domain); - error = dmar_unmap_buf_locked(domain, base, size, flags); + error = dmar_unmap_buf_locked(domain, entry->start, entry->end - + entry->start, flags); DMAR_DOMAIN_PGUNLOCK(domain); return (error); } int dmar_domain_alloc_pgtbl(struct dmar_domain *domain) { vm_page_t m; KASSERT(domain->pgtbl_obj == NULL, ("already initialized %p", domain)); domain->pgtbl_obj = vm_pager_allocate(OBJT_PHYS, NULL, IDX_TO_OFF(pglvl_max_pages(domain->pglvl)), 0, 0, NULL); DMAR_DOMAIN_PGLOCK(domain); m = iommu_pgalloc(domain->pgtbl_obj, 0, IOMMU_PGF_WAITOK | IOMMU_PGF_ZERO | IOMMU_PGF_OBJL); /* No implicit free of the top level page table page. */ vm_page_wire(m); DMAR_DOMAIN_PGUNLOCK(domain); DMAR_LOCK(domain->dmar); domain->iodom.flags |= IOMMU_DOMAIN_PGTBL_INITED; DMAR_UNLOCK(domain->dmar); return (0); } void dmar_domain_free_pgtbl(struct dmar_domain *domain) { vm_object_t obj; vm_page_t m; obj = domain->pgtbl_obj; if (obj == NULL) { KASSERT((domain->dmar->hw_ecap & DMAR_ECAP_PT) != 0 && (domain->iodom.flags & IOMMU_DOMAIN_IDMAP) != 0, ("lost pagetable object domain %p", domain)); return; } DMAR_DOMAIN_ASSERT_PGLOCKED(domain); domain->pgtbl_obj = NULL; if ((domain->iodom.flags & IOMMU_DOMAIN_IDMAP) != 0) { dmar_put_idmap_pgtbl(obj); domain->iodom.flags &= ~IOMMU_DOMAIN_IDMAP; return; } /* Obliterate ref_counts */ VM_OBJECT_ASSERT_WLOCKED(obj); for (m = vm_page_lookup(obj, 0); m != NULL; m = vm_page_next(m)) { vm_page_clearref(m); vm_wire_sub(1); } VM_OBJECT_WUNLOCK(obj); vm_object_deallocate(obj); } static inline uint64_t dmar_wait_iotlb_flush(struct dmar_unit *unit, uint64_t wt, int iro) { uint64_t iotlbr; dmar_write8(unit, iro + DMAR_IOTLB_REG_OFF, DMAR_IOTLB_IVT | DMAR_IOTLB_DR | DMAR_IOTLB_DW | wt); for (;;) { iotlbr = dmar_read8(unit, iro + DMAR_IOTLB_REG_OFF); if ((iotlbr & DMAR_IOTLB_IVT) == 0) break; cpu_spinwait(); } return (iotlbr); } void dmar_flush_iotlb_sync(struct dmar_domain *domain, iommu_gaddr_t base, iommu_gaddr_t size) { struct dmar_unit *unit; iommu_gaddr_t isize; uint64_t iotlbr; int am, iro; unit = domain->dmar; KASSERT(!unit->qi_enabled, ("dmar%d: sync iotlb flush call", unit->iommu.unit)); iro = DMAR_ECAP_IRO(unit->hw_ecap) * 16; DMAR_LOCK(unit); if ((unit->hw_cap & DMAR_CAP_PSI) == 0 || size > 2 * 1024 * 1024) { iotlbr = dmar_wait_iotlb_flush(unit, DMAR_IOTLB_IIRG_DOM | DMAR_IOTLB_DID(domain->domain), iro); KASSERT((iotlbr & DMAR_IOTLB_IAIG_MASK) != DMAR_IOTLB_IAIG_INVLD, ("dmar%d: invalidation failed %jx", unit->iommu.unit, (uintmax_t)iotlbr)); } else { for (; size > 0; base += isize, size -= isize) { am = calc_am(unit, base, size, &isize); dmar_write8(unit, iro, base | am); iotlbr = dmar_wait_iotlb_flush(unit, DMAR_IOTLB_IIRG_PAGE | DMAR_IOTLB_DID(domain->domain), iro); KASSERT((iotlbr & DMAR_IOTLB_IAIG_MASK) != DMAR_IOTLB_IAIG_INVLD, ("dmar%d: PSI invalidation failed " "iotlbr 0x%jx base 0x%jx size 0x%jx am %d", unit->iommu.unit, (uintmax_t)iotlbr, (uintmax_t)base, (uintmax_t)size, am)); /* * Any non-page granularity covers whole guest * address space for the domain. */ if ((iotlbr & DMAR_IOTLB_IAIG_MASK) != DMAR_IOTLB_IAIG_PAGE) break; } } DMAR_UNLOCK(unit); } const struct iommu_domain_map_ops dmar_domain_map_ops = { .map = dmar_map_buf, .unmap = dmar_unmap_buf, };