| // Copyright 2018 The Fuchsia Authors |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| #include "iommu_impl.h" |
| |
| #include <align.h> |
| #include <lib/root_resource_filter.h> |
| #include <platform.h> |
| #include <trace.h> |
| #include <zircon/errors.h> |
| #include <zircon/time.h> |
| #include <zircon/types.h> |
| |
| #include <new> |
| |
| #include <dev/interrupt.h> |
| #include <fbl/algorithm.h> |
| #include <fbl/ref_ptr.h> |
| #include <ktl/algorithm.h> |
| #include <ktl/iterator.h> |
| #include <ktl/move.h> |
| #include <ktl/unique_ptr.h> |
| #include <vm/vm_aspace.h> |
| #include <vm/vm_object_paged.h> |
| #include <vm/vm_object_physical.h> |
| |
| #include "context_table_state.h" |
| #include "device_context.h" |
| #include "hw.h" |
| |
| #define LOCAL_TRACE 0 |
| |
| namespace intel_iommu { |
| |
| IommuImpl::IommuImpl(volatile void* register_base, ktl::unique_ptr<const uint8_t[]> desc, |
| size_t desc_len) |
| : desc_(ktl::move(desc)), desc_len_(desc_len), mmio_(register_base) { |
| memset(&irq_block_, 0, sizeof(irq_block_)); |
| // desc_len_ is currently unused, but we stash it so we can use the length |
| // of it later in case we need it. This silences a warning in Clang. |
| desc_len_ = desc_len; |
| } |
| |
| zx_status_t IommuImpl::Create(ktl::unique_ptr<const uint8_t[]> desc_bytes, size_t desc_len, |
| fbl::RefPtr<Iommu>* out) { |
| zx_status_t status = ValidateIommuDesc(desc_bytes, desc_len); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| auto desc = reinterpret_cast<const zx_iommu_desc_intel_t*>(desc_bytes.get()); |
| const uint64_t register_base = desc->register_base; |
| |
| // Add the IOMMU registers to the system wide MMIO deny list. User mode code |
| // may not gain access to these registers under any circumstances, even when |
| // it has access to the root resource. |
| root_resource_filter_add_deny_region(register_base, PAGE_SIZE, ZX_RSRC_KIND_MMIO); |
| |
| auto kernel_aspace = VmAspace::kernel_aspace(); |
| void* vaddr; |
| status = kernel_aspace->AllocPhysical( |
| "iommu", PAGE_SIZE, &vaddr, PAGE_SIZE_SHIFT, register_base, 0, |
| ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE | ARCH_MMU_FLAG_UNCACHED); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| fbl::AllocChecker ac; |
| auto instance = |
| fbl::AdoptRef<IommuImpl>(new (&ac) IommuImpl(vaddr, ktl::move(desc_bytes), desc_len)); |
| if (!ac.check()) { |
| kernel_aspace->FreeRegion(reinterpret_cast<vaddr_t>(vaddr)); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| status = instance->Initialize(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| *out = ktl::move(instance); |
| return ZX_OK; |
| } |
| |
| IommuImpl::~IommuImpl() { |
| Guard<Mutex> guard{&lock_}; |
| |
| // We cannot unpin memory until translation is disabled |
| zx_status_t status = SetTranslationEnableLocked(false, ZX_TIME_INFINITE); |
| ASSERT(status == ZX_OK); |
| |
| DisableFaultsLocked(); |
| if (irq_block_.allocated) { |
| msi_register_handler(&irq_block_, 0, nullptr, nullptr); |
| msi_free_block(&irq_block_); |
| } |
| |
| // Before destroying the tables we must first unmap all entries from the device contexts as the |
| // unmapping must be done with lock_ held, whilst object destruction must be done without the |
| // lock beind held. |
| for (auto& table : context_tables_) { |
| table.UnmapAllFromDeviceContextsLocked(); |
| } |
| |
| // Need to free any context tables before mmio_ is unmapped (and before this destructor |
| // concludes) as the context_tables_ hold raw pointers back into us. As the destructors of the |
| // tables will call operations that acquire the lock_ we drop them with the lock temporarily |
| // released. |
| fbl::DoublyLinkedList<ktl::unique_ptr<ContextTableState>> tables = ktl::move(context_tables_); |
| guard.CallUnlocked([&tables] { tables.clear(); }); |
| |
| VmAspace::kernel_aspace()->FreeRegion(mmio_.base()); |
| } |
| |
| // Validate the IOMMU descriptor from userspace. |
| // |
| // The IOMMU descriptor contains an array of scopes; if whole_segment is false, |
| // the scopes represent all devices managed by the IOMMU. If whole_segment is |
| // true, the scopes represent devices not managed by the IOMMU. |
| // |
| // An entry in the list is described by a "scope" below. A scope |
| // identifies a single PCIe device. If the device is behind a bridge, it will be |
| // described using multiple "hops", one for each bridge in the way and one for |
| // the device itself. A hop identifies the address of a bridge on the path to |
| // the device, or (in the final entry) the address of the device itself. |
| // |
| // The descriptor also contains a list of "Reserved Memory Regions", which |
| // describes regions of physical address space that must be identity-mapped for |
| // specific devices to function correctly. There is typically one region for |
| // the i915 gpu (initial framebuffer) and one for the XHCI controller |
| // (scratch space for the BIOS before the OS takes ownership of the controller). |
| zx_status_t IommuImpl::ValidateIommuDesc(const ktl::unique_ptr<const uint8_t[]>& desc_bytes, |
| size_t desc_len) { |
| auto desc = reinterpret_cast<const zx_iommu_desc_intel_t*>(desc_bytes.get()); |
| |
| // Validate the size |
| if (desc_len < sizeof(*desc)) { |
| LTRACEF("desc too short: %zu < %zu\n", desc_len, sizeof(*desc)); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| static_assert(sizeof(desc->scope_bytes) < sizeof(size_t), |
| "if this changes, need to check for overflow"); |
| size_t actual_size = sizeof(*desc); |
| if (add_overflow(actual_size, desc->scope_bytes, &actual_size) || |
| add_overflow(actual_size, desc->reserved_memory_bytes, &actual_size) || |
| actual_size != desc_len) { |
| LTRACEF("desc size mismatch: %zu != %zu\n", desc_len, actual_size); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| // Validate scopes |
| if (desc->scope_bytes == 0 && !desc->whole_segment) { |
| LTRACEF("desc has no scopes\n"); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| const size_t num_scopes = desc->scope_bytes / sizeof(zx_iommu_desc_intel_scope_t); |
| size_t scope_bytes = num_scopes; |
| if (mul_overflow(scope_bytes, sizeof(zx_iommu_desc_intel_scope_t), &scope_bytes) || |
| scope_bytes != desc->scope_bytes) { |
| LTRACEF("desc has invalid scope_bytes field\n"); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| auto scopes = reinterpret_cast<zx_iommu_desc_intel_scope_t*>(reinterpret_cast<uintptr_t>(desc) + |
| sizeof(*desc)); |
| for (size_t i = 0; i < num_scopes; ++i) { |
| if (scopes[i].num_hops == 0) { |
| LTRACEF("desc scope %zu has no hops\n", i); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (scopes[i].num_hops > ktl::size(scopes[0].dev_func)) { |
| LTRACEF("desc scope %zu has too many hops\n", i); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| } |
| |
| // Validate reserved memory regions |
| size_t cursor_bytes = sizeof(*desc) + desc->scope_bytes; |
| while (cursor_bytes + sizeof(zx_iommu_desc_intel_reserved_memory_t) < desc_len) { |
| auto mem = reinterpret_cast<zx_iommu_desc_intel_reserved_memory_t*>( |
| reinterpret_cast<uintptr_t>(desc) + cursor_bytes); |
| |
| size_t next_entry = cursor_bytes; |
| if (add_overflow(next_entry, sizeof(zx_iommu_desc_intel_reserved_memory_t), &next_entry) || |
| add_overflow(next_entry, mem->scope_bytes, &next_entry) || next_entry > desc_len) { |
| LTRACEF("desc reserved memory entry has invalid scope_bytes\n"); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| // TODO(teisenbe): Make sure that the reserved memory regions are not in our |
| // allocatable RAM pools |
| |
| // Validate scopes |
| if (mem->scope_bytes == 0) { |
| LTRACEF("desc reserved memory entry has no scopes\n"); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| const size_t num_scopes = mem->scope_bytes / sizeof(zx_iommu_desc_intel_scope_t); |
| size_t scope_bytes = num_scopes; |
| if (mul_overflow(scope_bytes, sizeof(zx_iommu_desc_intel_scope_t), &scope_bytes) || |
| scope_bytes != desc->scope_bytes) { |
| LTRACEF("desc reserved memory entry has invalid scope_bytes field\n"); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| auto scopes = reinterpret_cast<zx_iommu_desc_intel_scope_t*>(reinterpret_cast<uintptr_t>(mem) + |
| sizeof(*mem)); |
| for (size_t i = 0; i < num_scopes; ++i) { |
| if (scopes[i].num_hops == 0) { |
| LTRACEF("desc reserved memory entry scope %zu has no hops\n", i); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (scopes[i].num_hops > ktl::size(scopes[0].dev_func)) { |
| LTRACEF("desc reserved memory entry scope %zu has too many hops\n", i); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| } |
| |
| cursor_bytes = next_entry; |
| } |
| if (cursor_bytes != desc_len) { |
| LTRACEF("desc has invalid reserved_memory_bytes field\n"); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| LTRACEF("validated desc\n"); |
| return ZX_OK; |
| } |
| |
| bool IommuImpl::IsValidBusTxnId(uint64_t bus_txn_id) const { |
| if (bus_txn_id > UINT16_MAX) { |
| return false; |
| } |
| |
| ds::Bdf bdf = decode_bus_txn_id(bus_txn_id); |
| |
| auto desc = reinterpret_cast<const zx_iommu_desc_intel_t*>(desc_.get()); |
| const size_t num_scopes = desc->scope_bytes / sizeof(zx_iommu_desc_intel_scope_t); |
| auto scopes = reinterpret_cast<zx_iommu_desc_intel_scope_t*>(reinterpret_cast<uintptr_t>(desc) + |
| sizeof(*desc)); |
| |
| // Search for this BDF in the scopes we have |
| for (size_t i = 0; i < num_scopes; ++i) { |
| if (scopes[i].num_hops != 1) { |
| // TODO(teisenbe): Implement |
| continue; |
| } |
| |
| if (scopes[i].start_bus == bdf.bus() && scopes[i].dev_func[0] == bdf.packed_dev_and_func()) { |
| return !desc->whole_segment; |
| } |
| } |
| |
| if (desc->whole_segment) { |
| // Since we only support single segment currently, just return true |
| // here. To support more segments, we need to make sure the segment |
| // matches, too. |
| return true; |
| } |
| |
| return false; |
| } |
| |
| zx_status_t IommuImpl::Map(uint64_t bus_txn_id, const fbl::RefPtr<VmObject>& vmo, uint64_t offset, |
| size_t size, uint32_t perms, dev_vaddr_t* vaddr, size_t* mapped_len) { |
| DEBUG_ASSERT(vaddr); |
| if (!IS_PAGE_ALIGNED(offset) || size == 0) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (perms & ~(IOMMU_FLAG_PERM_READ | IOMMU_FLAG_PERM_WRITE | IOMMU_FLAG_PERM_EXECUTE)) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (perms == 0) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (!IsValidBusTxnId(bus_txn_id)) { |
| return ZX_ERR_NOT_FOUND; |
| } |
| |
| ds::Bdf bdf = decode_bus_txn_id(bus_txn_id); |
| |
| Guard<Mutex> guard{&lock_}; |
| DeviceContext* dev; |
| zx_status_t status = GetOrCreateDeviceContextLocked(bdf, &dev); |
| if (status != ZX_OK) { |
| return status; |
| } |
| return dev->SecondLevelMap(vmo, offset, size, perms, false /* map_contiguous */, vaddr, |
| mapped_len); |
| } |
| |
| zx_status_t IommuImpl::MapContiguous(uint64_t bus_txn_id, const fbl::RefPtr<VmObject>& vmo, |
| uint64_t offset, size_t size, uint32_t perms, |
| dev_vaddr_t* vaddr, size_t* mapped_len) { |
| DEBUG_ASSERT(vaddr); |
| if (!IS_PAGE_ALIGNED(offset) || size == 0) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (perms & ~(IOMMU_FLAG_PERM_READ | IOMMU_FLAG_PERM_WRITE | IOMMU_FLAG_PERM_EXECUTE)) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (perms == 0) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (!IsValidBusTxnId(bus_txn_id)) { |
| return ZX_ERR_NOT_FOUND; |
| } |
| |
| ds::Bdf bdf = decode_bus_txn_id(bus_txn_id); |
| |
| Guard<Mutex> guard{&lock_}; |
| DeviceContext* dev; |
| zx_status_t status = GetOrCreateDeviceContextLocked(bdf, &dev); |
| if (status != ZX_OK) { |
| return status; |
| } |
| return dev->SecondLevelMap(vmo, offset, size, perms, true /* map_contiguous */, vaddr, |
| mapped_len); |
| } |
| |
| zx_status_t IommuImpl::Unmap(uint64_t bus_txn_id, dev_vaddr_t vaddr, size_t size) { |
| if (!IS_PAGE_ALIGNED(vaddr) || !IS_PAGE_ALIGNED(size)) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (!IsValidBusTxnId(bus_txn_id)) { |
| return ZX_ERR_NOT_FOUND; |
| } |
| |
| ds::Bdf bdf = decode_bus_txn_id(bus_txn_id); |
| |
| Guard<Mutex> guard{&lock_}; |
| DeviceContext* dev; |
| zx_status_t status = GetOrCreateDeviceContextLocked(bdf, &dev); |
| if (status != ZX_OK) { |
| return status; |
| } |
| status = dev->SecondLevelUnmap(vaddr, size); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t IommuImpl::ClearMappingsForBusTxnId(uint64_t bus_txn_id) { |
| PANIC_UNIMPLEMENTED; |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t IommuImpl::Initialize() { |
| Guard<Mutex> guard{&lock_}; |
| |
| // Ensure we support this device version |
| auto version = reg::Version::Get().ReadFrom(&mmio_); |
| if (version.major() != 1 && version.minor() != 0) { |
| LTRACEF("Unsupported IOMMU version: %u.%u\n", version.major(), version.minor()); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| // Cache useful capability info |
| caps_ = reg::Capability::Get().ReadFrom(&mmio_); |
| extended_caps_ = reg::ExtendedCapability::Get().ReadFrom(&mmio_); |
| |
| max_guest_addr_mask_ = (1ULL << (caps_.max_guest_addr_width() + 1)) - 1; |
| fault_recording_reg_offset_ = static_cast<uint32_t>(caps_.fault_recording_register_offset() * 16); |
| num_fault_recording_reg_ = static_cast<uint32_t>(caps_.num_fault_recording_reg() + 1); |
| iotlb_reg_offset_ = static_cast<uint32_t>(extended_caps_.iotlb_register_offset() * 16); |
| |
| constexpr size_t kIoTlbRegisterBankSize = 16; |
| if (iotlb_reg_offset_ > PAGE_SIZE - kIoTlbRegisterBankSize) { |
| LTRACEF("Unsupported IOMMU: IOTLB offset runs past the register page\n"); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| supports_extended_context_ = extended_caps_.supports_extended_context(); |
| if (extended_caps_.supports_pasid()) { |
| valid_pasid_mask_ = static_cast<uint32_t>((1ULL << (extended_caps_.pasid_size() + 1)) - 1); |
| } |
| |
| const uint64_t num_domains_raw = caps_.num_domains(); |
| if (num_domains_raw > 0x6) { |
| LTRACEF("Unknown num_domains value\n"); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| const uint32_t num_supported_domains = static_cast<uint32_t>(1ul << (4 + 2 * num_domains_raw)); |
| domain_allocator_.set_num_domains(num_supported_domains); |
| |
| // Sanity check initial configuration |
| auto global_ctl = reg::GlobalControl::Get().ReadFrom(&mmio_); |
| if (global_ctl.translation_enable()) { |
| LTRACEF("DMA remapping already enabled?!\n"); |
| return ZX_ERR_BAD_STATE; |
| } |
| if (global_ctl.interrupt_remap_enable()) { |
| LTRACEF("IRQ remapping already enabled?!\n"); |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| // Allocate and setup the root table |
| zx_status_t status = IommuPage::AllocatePage(&root_table_page_); |
| if (status != ZX_OK) { |
| LTRACEF("alloc root table failed\n"); |
| return status; |
| } |
| status = SetRootTablePointerLocked(root_table_page_.paddr()); |
| if (status != ZX_OK) { |
| LTRACEF("set root table failed\n"); |
| return status; |
| } |
| |
| // Enable interrupts before we enable translation |
| status = ConfigureFaultEventInterruptLocked(); |
| if (status != ZX_OK) { |
| LTRACEF("configuring fault event irq failed\n"); |
| return status; |
| } |
| |
| status = EnableBiosReservedMappingsLocked(); |
| if (status != ZX_OK) { |
| LTRACEF("enable bios reserved mappings failed\n"); |
| return status; |
| } |
| |
| status = SetTranslationEnableLocked(true, zx_time_add_duration(current_time(), ZX_SEC(1))); |
| if (status != ZX_OK) { |
| LTRACEF("set translation enable failed\n"); |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t IommuImpl::EnableBiosReservedMappingsLocked() { |
| auto desc = reinterpret_cast<const zx_iommu_desc_intel_t*>(desc_.get()); |
| |
| size_t cursor_bytes = 0; |
| while (cursor_bytes + sizeof(zx_iommu_desc_intel_reserved_memory_t) < |
| desc->reserved_memory_bytes) { |
| // The descriptor has already been validated, so no need to check again. |
| auto mem = reinterpret_cast<zx_iommu_desc_intel_reserved_memory_t*>( |
| reinterpret_cast<uintptr_t>(desc) + sizeof(*desc) + desc->scope_bytes + cursor_bytes); |
| |
| const size_t num_scopes = mem->scope_bytes / sizeof(zx_iommu_desc_intel_scope_t); |
| auto scopes = reinterpret_cast<zx_iommu_desc_intel_scope_t*>(reinterpret_cast<uintptr_t>(mem) + |
| sizeof(*mem)); |
| for (size_t i = 0; i < num_scopes; ++i) { |
| if (scopes[i].num_hops != 1) { |
| // TODO(teisenbe): Implement |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| ds::Bdf bdf; |
| bdf.set_bus(scopes[i].start_bus); |
| bdf.set_dev(static_cast<uint8_t>(scopes[i].dev_func[0] >> 3)); |
| bdf.set_func(static_cast<uint8_t>(scopes[i].dev_func[0] & 0x7)); |
| |
| DeviceContext* dev; |
| zx_status_t status = GetOrCreateDeviceContextLocked(bdf, &dev); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| LTRACEF("Enabling region [%lx, %lx) for %02x:%02x.%02x\n", mem->base_addr, |
| mem->base_addr + mem->len, bdf.bus(), bdf.dev(), bdf.func()); |
| size_t size = ROUNDUP(mem->len, PAGE_SIZE); |
| const uint32_t perms = IOMMU_FLAG_PERM_READ | IOMMU_FLAG_PERM_WRITE; |
| status = dev->SecondLevelMapIdentity(mem->base_addr, size, perms); |
| if (status != ZX_OK) { |
| return status; |
| } |
| } |
| |
| cursor_bytes += sizeof(*mem) + mem->scope_bytes; |
| } |
| |
| return ZX_OK; |
| } |
| |
| // Sets the root table pointer and invalidates the context-cache and IOTLB. |
| zx_status_t IommuImpl::SetRootTablePointerLocked(paddr_t pa) { |
| DEBUG_ASSERT(IS_PAGE_ALIGNED(pa)); |
| |
| auto root_table_addr = reg::RootTableAddress::Get().FromValue(0); |
| // If we support extended contexts, use it. |
| root_table_addr.set_root_table_type(supports_extended_context_); |
| root_table_addr.set_root_table_address(pa >> PAGE_SIZE_SHIFT); |
| root_table_addr.WriteTo(&mmio_); |
| |
| auto global_ctl = reg::GlobalControl::Get().ReadFrom(&mmio_); |
| DEBUG_ASSERT(!global_ctl.translation_enable()); |
| global_ctl.set_root_table_ptr(1); |
| global_ctl.WriteTo(&mmio_); |
| zx_status_t status = WaitForValueLocked(&global_ctl, &decltype(global_ctl)::root_table_ptr, 1, |
| zx_time_add_duration(current_time(), ZX_SEC(1))); |
| if (status != ZX_OK) { |
| LTRACEF("Timed out waiting for root_table_ptr bit to take\n"); |
| return status; |
| } |
| |
| InvalidateContextCacheGlobalLocked(); |
| InvalidateIotlbGlobalLocked(); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t IommuImpl::SetTranslationEnableLocked(bool enabled, zx_time_t deadline) { |
| auto global_ctl = reg::GlobalControl::Get().ReadFrom(&mmio_); |
| global_ctl.set_translation_enable(enabled); |
| global_ctl.WriteTo(&mmio_); |
| |
| return WaitForValueLocked(&global_ctl, &decltype(global_ctl)::translation_enable, enabled, |
| deadline); |
| } |
| |
| void IommuImpl::InvalidateContextCacheGlobalLocked() { |
| DEBUG_ASSERT(lock_.lock().IsHeld()); |
| |
| auto context_cmd = reg::ContextCommand::Get().FromValue(0); |
| context_cmd.set_invld_context_cache(1); |
| context_cmd.set_invld_request_granularity(reg::ContextCommand::kGlobalInvld); |
| context_cmd.WriteTo(&mmio_); |
| |
| WaitForValueLocked(&context_cmd, &decltype(context_cmd)::invld_context_cache, 0, |
| ZX_TIME_INFINITE); |
| } |
| |
| void IommuImpl::InvalidateContextCacheDomainLocked(uint32_t domain_id) { |
| DEBUG_ASSERT(lock_.lock().IsHeld()); |
| |
| auto context_cmd = reg::ContextCommand::Get().FromValue(0); |
| context_cmd.set_invld_context_cache(1); |
| context_cmd.set_invld_request_granularity(reg::ContextCommand::kDomainInvld); |
| context_cmd.set_domain_id(domain_id); |
| context_cmd.WriteTo(&mmio_); |
| |
| WaitForValueLocked(&context_cmd, &decltype(context_cmd)::invld_context_cache, 0, |
| ZX_TIME_INFINITE); |
| } |
| |
| void IommuImpl::InvalidateContextCacheGlobal() { |
| Guard<Mutex> guard{&lock_}; |
| InvalidateContextCacheGlobalLocked(); |
| } |
| |
| void IommuImpl::InvalidateContextCacheDomain(uint32_t domain_id) { |
| Guard<Mutex> guard{&lock_}; |
| InvalidateContextCacheDomainLocked(domain_id); |
| } |
| |
| void IommuImpl::InvalidateIotlbGlobalLocked() { |
| DEBUG_ASSERT(lock_.lock().IsHeld()); |
| ASSERT(!caps_.required_write_buf_flushing()); |
| |
| // TODO(teisenbe): Read/write draining? |
| auto iotlb_invld = reg::IotlbInvalidate::Get(iotlb_reg_offset_).ReadFrom(&mmio_); |
| iotlb_invld.set_invld_iotlb(1); |
| iotlb_invld.set_invld_request_granularity(reg::IotlbInvalidate::kGlobalInvld); |
| iotlb_invld.WriteTo(&mmio_); |
| |
| WaitForValueLocked(&iotlb_invld, &decltype(iotlb_invld)::invld_iotlb, 0, ZX_TIME_INFINITE); |
| } |
| |
| void IommuImpl::InvalidateIotlbDomainAllLocked(uint32_t domain_id) { |
| DEBUG_ASSERT(lock_.lock().IsHeld()); |
| ASSERT(!caps_.required_write_buf_flushing()); |
| |
| // TODO(teisenbe): Read/write draining? |
| auto iotlb_invld = reg::IotlbInvalidate::Get(iotlb_reg_offset_).ReadFrom(&mmio_); |
| iotlb_invld.set_invld_iotlb(1); |
| iotlb_invld.set_invld_request_granularity(reg::IotlbInvalidate::kDomainAllInvld); |
| iotlb_invld.set_domain_id(domain_id); |
| iotlb_invld.WriteTo(&mmio_); |
| |
| WaitForValueLocked(&iotlb_invld, &decltype(iotlb_invld)::invld_iotlb, 0, ZX_TIME_INFINITE); |
| } |
| |
| void IommuImpl::InvalidateIotlbPageLocked(uint32_t domain_id, dev_vaddr_t vaddr, uint pages_pow2) { |
| DEBUG_ASSERT(lock_.lock().IsHeld()); |
| DEBUG_ASSERT(IS_PAGE_ALIGNED(vaddr)); |
| DEBUG_ASSERT(pages_pow2 < 64); |
| DEBUG_ASSERT(pages_pow2 <= caps_.max_addr_mask_value()); |
| ASSERT(!caps_.required_write_buf_flushing()); |
| |
| auto invld_addr = reg::InvalidateAddress::Get(iotlb_reg_offset_).FromValue(0); |
| invld_addr.set_address(vaddr >> 12); |
| invld_addr.set_invld_hint(0); |
| invld_addr.set_address_mask(pages_pow2); |
| invld_addr.WriteTo(&mmio_); |
| |
| // TODO(teisenbe): Read/write draining? |
| auto iotlb_invld = reg::IotlbInvalidate::Get(iotlb_reg_offset_).ReadFrom(&mmio_); |
| iotlb_invld.set_invld_iotlb(1); |
| iotlb_invld.set_invld_request_granularity(reg::IotlbInvalidate::kDomainPageInvld); |
| iotlb_invld.set_domain_id(domain_id); |
| iotlb_invld.WriteTo(&mmio_); |
| |
| WaitForValueLocked(&iotlb_invld, &decltype(iotlb_invld)::invld_iotlb, 0, ZX_TIME_INFINITE); |
| } |
| |
| void IommuImpl::InvalidateIotlbGlobal() { |
| Guard<Mutex> guard{&lock_}; |
| InvalidateIotlbGlobalLocked(); |
| } |
| |
| void IommuImpl::InvalidateIotlbDomainAll(uint32_t domain_id) { |
| Guard<Mutex> guard{&lock_}; |
| InvalidateIotlbDomainAllLocked(domain_id); |
| } |
| |
| template <class RegType> |
| zx_status_t IommuImpl::WaitForValueLocked(RegType* reg, |
| typename RegType::ValueType (RegType::*getter)() const, |
| typename RegType::ValueType value, zx_time_t deadline) { |
| DEBUG_ASSERT(lock_.lock().IsHeld()); |
| |
| const zx_time_t kMaxSleepDuration = ZX_USEC(10); |
| |
| while (true) { |
| // Read the register and check if it matches the expected value. If |
| // not, sleep for a bit and try again. |
| reg->ReadFrom(&mmio_); |
| if ((reg->*getter)() == value) { |
| return ZX_OK; |
| } |
| |
| const zx_time_t now = current_time(); |
| if (now > deadline) { |
| break; |
| } |
| |
| zx_time_t sleep_deadline = ktl::min(zx_time_add_duration(now, kMaxSleepDuration), deadline); |
| Thread::Current::Sleep(sleep_deadline); |
| } |
| return ZX_ERR_TIMED_OUT; |
| } |
| |
| interrupt_eoi IommuImpl::FaultHandler(void* ctx) { |
| auto self = static_cast<IommuImpl*>(ctx); |
| auto status = reg::FaultStatus::Get().ReadFrom(&self->mmio_); |
| |
| if (!status.primary_pending_fault()) { |
| TRACEF("Non primary fault\n"); |
| return IRQ_EOI_DEACTIVATE; |
| } |
| |
| auto caps = reg::Capability::Get().ReadFrom(&self->mmio_); |
| const uint32_t num_regs = static_cast<uint32_t>(caps.num_fault_recording_reg() + 1); |
| const uint32_t reg_offset = static_cast<uint32_t>(caps.fault_recording_register_offset() * 16); |
| |
| uint32_t index = status.fault_record_index(); |
| while (1) { |
| auto rec_high = reg::FaultRecordHigh::Get(reg_offset, index).ReadFrom(&self->mmio_); |
| if (!rec_high.fault()) { |
| break; |
| } |
| auto rec_low = reg::FaultRecordLow::Get(reg_offset, index).ReadFrom(&self->mmio_); |
| uint64_t source = rec_high.source_id(); |
| TRACEF( |
| "IOMMU Fault: access %c, PASID (%c) %#04lx, reason %#02lx, source %02lx:%02lx.%lx, info: " |
| "%lx\n", |
| rec_high.request_type() ? 'R' : 'W', rec_high.pasid_present() ? 'V' : '-', |
| rec_high.pasid_value(), rec_high.fault_reason(), source >> 8, (source >> 3) & 0x1f, |
| source & 0x7, rec_low.fault_info() << 12); |
| |
| // Clear this fault (RW1CS) |
| rec_high.WriteTo(&self->mmio_); |
| |
| ++index; |
| if (index >= num_regs) { |
| index -= num_regs; |
| } |
| } |
| |
| status.set_reg_value(0); |
| // Clear the primary fault overflow condition (RW1CS) |
| // TODO(teisenbe): How do we guarantee we get an interrupt on the next fault/if we left a fault |
| // unprocessed? |
| status.set_primary_fault_overflow(1); |
| status.WriteTo(&self->mmio_); |
| return IRQ_EOI_DEACTIVATE; |
| } |
| |
| zx_status_t IommuImpl::ConfigureFaultEventInterruptLocked() { |
| DEBUG_ASSERT(lock_.lock().IsHeld()); |
| |
| if (!msi_is_supported()) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| DEBUG_ASSERT(!irq_block_.allocated); |
| zx_status_t status = |
| msi_alloc_block(1, false /* can_target_64bit */, false /* msi x */, &irq_block_); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| auto event_data = reg::FaultEventData::Get().FromValue(irq_block_.tgt_data); |
| auto event_addr = |
| reg::FaultEventAddress::Get().FromValue(static_cast<uint32_t>(irq_block_.tgt_addr)); |
| auto event_upper_addr = reg::FaultEventUpperAddress::Get().FromValue( |
| static_cast<uint32_t>(irq_block_.tgt_addr >> 32)); |
| |
| event_data.WriteTo(&mmio_); |
| event_addr.WriteTo(&mmio_); |
| event_upper_addr.WriteTo(&mmio_); |
| |
| // Clear all primary fault records |
| for (uint32_t i = 0; i < num_fault_recording_reg_; ++i) { |
| const uint32_t offset = fault_recording_reg_offset_; |
| auto record_high = reg::FaultRecordHigh::Get(offset, i).ReadFrom(&mmio_); |
| record_high.WriteTo(&mmio_); |
| } |
| |
| // Clear all pending faults |
| auto fault_status_ctl = reg::FaultStatus::Get().ReadFrom(&mmio_); |
| fault_status_ctl.WriteTo(&mmio_); |
| |
| msi_register_handler(&irq_block_, 0, FaultHandler, this); |
| |
| // Unmask interrupts |
| auto fault_event_ctl = reg::FaultEventControl::Get().ReadFrom(&mmio_); |
| fault_event_ctl.set_interrupt_mask(0); |
| fault_event_ctl.WriteTo(&mmio_); |
| |
| return ZX_OK; |
| } |
| |
| void IommuImpl::DisableFaultsLocked() { |
| auto fault_event_ctl = reg::FaultEventControl::Get().ReadFrom(&mmio_); |
| fault_event_ctl.set_interrupt_mask(1); |
| fault_event_ctl.WriteTo(&mmio_); |
| } |
| |
| zx_status_t IommuImpl::GetOrCreateContextTableLocked(ds::Bdf bdf, ContextTableState** tbl) { |
| DEBUG_ASSERT(lock_.lock().IsHeld()); |
| |
| volatile ds::RootTable* root_table = this->root_table(); |
| DEBUG_ASSERT(root_table); |
| |
| volatile ds::RootEntrySubentry* target_entry = &root_table->entry[bdf.bus()].lower; |
| if (supports_extended_context_ && bdf.dev() >= 16) { |
| // If this is an extended root table and the device is in the upper half |
| // of the bus address space, use the upper pointer. |
| target_entry = &root_table->entry[bdf.bus()].upper; |
| } |
| |
| ds::RootEntrySubentry entry; |
| entry.ReadFrom(target_entry); |
| if (entry.present()) { |
| // We know the entry exists, so search our list of tables for it. |
| for (ContextTableState& context_table : context_tables_) { |
| if (context_table.includes_bdf(bdf)) { |
| *tbl = &context_table; |
| return ZX_OK; |
| } |
| } |
| } |
| |
| // Couldn't find the ContextTable, so create it. |
| ktl::unique_ptr<ContextTableState> table; |
| zx_status_t status = |
| ContextTableState::Create(static_cast<uint8_t>(bdf.bus()), supports_extended_context_, |
| bdf.dev() >= 16 /* upper */, this, target_entry, &table); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| *tbl = table.get(); |
| context_tables_.push_back(ktl::move(table)); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t IommuImpl::GetOrCreateDeviceContextLocked(ds::Bdf bdf, DeviceContext** context) { |
| DEBUG_ASSERT(lock_.lock().IsHeld()); |
| |
| ContextTableState* ctx_table_state; |
| zx_status_t status = GetOrCreateContextTableLocked(bdf, &ctx_table_state); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| status = ctx_table_state->GetDeviceContext(bdf, context); |
| if (status != ZX_ERR_NOT_FOUND) { |
| // Either status was ZX_OK and we're done, or some error occurred. |
| return status; |
| } |
| |
| uint32_t domain_id; |
| status = domain_allocator_.Allocate(&domain_id); |
| if (status != ZX_OK) { |
| return status; |
| } |
| return ctx_table_state->CreateDeviceContext(bdf, domain_id, context); |
| } |
| |
| uint64_t IommuImpl::minimum_contiguity(uint64_t bus_txn_id) { |
| if (!IsValidBusTxnId(bus_txn_id)) { |
| return 0; |
| } |
| |
| ds::Bdf bdf = decode_bus_txn_id(bus_txn_id); |
| |
| Guard<Mutex> guard{&lock_}; |
| DeviceContext* dev; |
| zx_status_t status = GetOrCreateDeviceContextLocked(bdf, &dev); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| return dev->minimum_contiguity(); |
| } |
| |
| uint64_t IommuImpl::aspace_size(uint64_t bus_txn_id) { |
| if (!IsValidBusTxnId(bus_txn_id)) { |
| return 0; |
| } |
| |
| ds::Bdf bdf = decode_bus_txn_id(bus_txn_id); |
| |
| Guard<Mutex> guard{&lock_}; |
| DeviceContext* dev; |
| zx_status_t status = GetOrCreateDeviceContextLocked(bdf, &dev); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| return dev->aspace_size(); |
| } |
| |
| } // namespace intel_iommu |