| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/devices/lib/iommu/iommu-x86.h" |
| |
| #include <lib/ddk/driver.h> |
| #include <zircon/status.h> |
| |
| #include <optional> |
| |
| #include <fbl/auto_lock.h> |
| |
| #define logf(severity, message...) Logf((FX_LOG_##severity), __FILE__, __LINE__, message) |
| |
| namespace { |
| |
| // Temporary single global reference until C wrappers are removed. |
| x86::IommuManager* iommu_mgr = nullptr; |
| |
| bool use_hardware_iommu(void) { |
| char value[32]; |
| auto status = device_get_variable(nullptr, "driver.iommu.enable", value, sizeof(value), nullptr); |
| if (status != ZX_OK) { |
| return false; // Default to false currently |
| } else if (!strcmp(value, "0") || !strcmp(value, "false") || !strcmp(value, "off")) { |
| return false; |
| } else { |
| return true; |
| } |
| } |
| |
| // Given a table that may have a Length > sizeof(TABLE), this returns a Span of the data following |
| // table, based on that Length. The type T of the record can be used to get a more convenient span |
| // and will cause the alignment etc to ber checked. |
| template <typename T, bool HEADER = true, typename TABLE> |
| std::optional<cpp20::span<const T>> record_span(const TABLE* table) { |
| const uintptr_t records_start = reinterpret_cast<uintptr_t>(table) + sizeof(*table); |
| size_t size; |
| if constexpr (HEADER) { |
| size = table->Header.Length; |
| } else { |
| size = table->Length; |
| } |
| const uintptr_t records_end = reinterpret_cast<uintptr_t>(table) + size; |
| |
| size_t byte_len = records_end - records_start; |
| if ((byte_len % sizeof(T)) != 0 || (records_start % std::alignment_of_v<T>) != 0) { |
| return std::nullopt; |
| } |
| return cpp20::span<const T>{reinterpret_cast<const T*>(records_start), byte_len / sizeof(T)}; |
| } |
| |
| // Given a TABLE this will iterate over all the variable length records that might follow it. |
| // That is RECORD::Length is the length of each record. The provided func will be called on every |
| // record that is found. |
| template <typename RECORD, typename TABLE, typename FUNC> |
| zx_status_t for_each_record(const TABLE* table, FUNC func) { |
| cpp20::span<const uint8_t> records; |
| if (auto r = record_span<uint8_t>(table)) { |
| records = std::move(*r); |
| } else { |
| return ZX_ERR_IO_DATA_INTEGRITY; |
| } |
| |
| uintptr_t offset; |
| for (offset = 0; offset < records.size();) { |
| const RECORD* record_hdr = reinterpret_cast<const RECORD*>(&records[offset]); |
| zx_status_t result = func(record_hdr); |
| if (result != ZX_ERR_NEXT) { |
| return result; |
| } |
| |
| offset += record_hdr->Length; |
| } |
| |
| if (offset != records.size()) { |
| return ZX_ERR_IO_DATA_INTEGRITY; |
| } |
| |
| return ZX_OK; |
| } |
| |
| // Convenience wrapper around for_each_record that filters for the requested record type and calls |
| // the provided func with a pointer to the correctly typed table. |
| template <AcpiDmarType Type, typename F> |
| zx_status_t for_each_dmar_of_type(const ACPI_TABLE_DMAR* dmar, F func) { |
| return for_each_record<ACPI_DMAR_HEADER>(dmar, [func](const ACPI_DMAR_HEADER* h) { |
| if (h->Type == Type) { |
| if constexpr (Type == ACPI_DMAR_TYPE_HARDWARE_UNIT) { |
| const ACPI_DMAR_HARDWARE_UNIT* r = reinterpret_cast<const ACPI_DMAR_HARDWARE_UNIT*>(h); |
| return func(r); |
| } else if constexpr (Type == ACPI_DMAR_TYPE_RESERVED_MEMORY) { |
| const ACPI_DMAR_RESERVED_MEMORY* m = reinterpret_cast<const ACPI_DMAR_RESERVED_MEMORY*>(h); |
| return func(m); |
| } else { |
| static_assert(Type != Type, |
| "for_each_dmar_of_type used for an unknown type. Please add it to the " |
| "if/else-if chain above"); |
| } |
| } |
| return ZX_ERR_NEXT; |
| }); |
| } |
| |
| // Converts a scope as described in the ACPI tables to one of the form used by zircon on x86. |
| zx_status_t acpi_scope_to_desc(const ACPI_DMAR_DEVICE_SCOPE* acpi_scope, |
| zx_iommu_desc_intel_scope_t* desc_scope) { |
| switch (acpi_scope->EntryType) { |
| case ACPI_DMAR_SCOPE_TYPE_ENDPOINT: |
| desc_scope->type = ZX_IOMMU_INTEL_SCOPE_ENDPOINT; |
| break; |
| case ACPI_DMAR_SCOPE_TYPE_BRIDGE: |
| return ZX_ERR_NOT_SUPPORTED; |
| default: |
| // Skip this scope, since it's not a type we care about. |
| return ZX_ERR_WRONG_TYPE; |
| } |
| desc_scope->start_bus = acpi_scope->Bus; |
| if (acpi_scope->Length < sizeof(*acpi_scope)) { |
| return ZX_ERR_IO_DATA_INTEGRITY; |
| } |
| |
| cpp20::span<const uint16_t> hops; |
| if (auto h = record_span<uint16_t, false>(acpi_scope)) { |
| hops = std::move(*h); |
| } else { |
| return ZX_ERR_IO_DATA_INTEGRITY; |
| } |
| |
| desc_scope->num_hops = static_cast<uint8_t>(hops.size()); |
| if (std::size(desc_scope->dev_func) < desc_scope->num_hops) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| // TODO(teisenbe): We need to be aware of the mapping between |
| // PCI paths and bus numbers to properly evaluate this. |
| if (desc_scope->num_hops != 1) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| // Walk the variable-length array of hops that is appended to the main |
| // ACPI_DMAR_DEVICE_SCOPE structure. |
| for (ssize_t i = 0; i < desc_scope->num_hops; ++i) { |
| uint16_t v = hops[i]; |
| const uint8_t dev = v & 0x1f; |
| const uint8_t func = (v >> 8) & 0x7; |
| desc_scope->dev_func[i] = static_cast<uint8_t>((dev << 3) | func); |
| } |
| return ZX_OK; |
| } |
| |
| // Walks the given unit's scopes and calls the supplied closure on each of them. This can be used |
| // on any TABLE where the records that follow it are scope records. |
| template <typename TABLE, typename F> |
| zx_status_t for_each_scope(const TABLE* unit, F func) { |
| return for_each_record<ACPI_DMAR_DEVICE_SCOPE>(unit, [func](const ACPI_DMAR_DEVICE_SCOPE* scope) { |
| zx_iommu_desc_intel_scope_t intel_scope; |
| zx_status_t status = acpi_scope_to_desc(scope, &intel_scope); |
| if (status == ZX_ERR_WRONG_TYPE) { |
| return ZX_ERR_NEXT; |
| } |
| if (status == ZX_OK) { |
| status = func(intel_scope); |
| } |
| return status; |
| }); |
| } |
| |
| bool scope_eq(const zx_iommu_desc_intel_scope_t* scope, |
| const zx_iommu_desc_intel_scope_t* other_scope) { |
| if (scope->type != other_scope->type || scope->start_bus != other_scope->start_bus || |
| scope->num_hops != other_scope->num_hops) { |
| return false; |
| } |
| |
| for (size_t i = 0; i < scope->num_hops; ++i) { |
| if (scope->dev_func[i] != other_scope->dev_func[i]) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| // Walks all the reserved memory regions and finds any that match the scopes for the provided |
| // pci_segment. The append_mem and append_scope callbacks are called on matching scopes/memory |
| // regions. See CreateDesc for an explanation of scope_func. |
| template <typename SCOPE_DESC, typename APPEND_MEM, typename APPEND_SCOPE> |
| zx_status_t process_reserved_mem(const ACPI_TABLE_DMAR* table, uint16_t pci_segment, |
| bool whole_segment, SCOPE_DESC scope_func, APPEND_MEM append_mem, |
| APPEND_SCOPE append_scope) { |
| return for_each_dmar_of_type<ACPI_DMAR_TYPE_RESERVED_MEMORY>( |
| table, [pci_segment, whole_segment, scope_func, append_mem, append_scope](const auto* rec) { |
| if (pci_segment != rec->Segment) { |
| return ZX_ERR_NEXT; |
| } |
| bool one_scope = false; |
| auto submit_scope = [&one_scope, append_scope, append_mem, base = rec->BaseAddress, |
| end = rec->EndAddress](const auto* scope) { |
| if (!one_scope) { |
| append_mem(base, end); |
| one_scope = true; |
| } |
| append_scope(scope); |
| }; |
| |
| // Search for scopes that match |
| zx_status_t result = for_each_scope<ACPI_DMAR_RESERVED_MEMORY>( |
| rec, [&whole_segment, &submit_scope, &scope_func](const auto& s) { |
| // TODO(teisenbe): We should skip scope types we don't |
| // care about here |
| |
| // Search for a scope in the descriptor that matches this |
| // ACPI scope. |
| bool no_matches = true; |
| scope_func([&s, &no_matches, &whole_segment, &submit_scope](const auto& scope) { |
| const bool scope_matches = scope_eq(&scope, &s); |
| no_matches &= !scope_matches; |
| |
| // If this is a whole segment descriptor, then a match |
| // corresponds to an entry we should ignore. |
| if (scope_matches && !whole_segment) { |
| submit_scope(&scope); |
| return ZX_ERR_STOP; |
| } |
| return ZX_ERR_NEXT; |
| }); |
| |
| if (no_matches && whole_segment) { |
| submit_scope(&s); |
| } |
| return ZX_ERR_NEXT; |
| }); |
| if (result != ZX_OK) { |
| return result; |
| } |
| return ZX_ERR_NEXT; |
| }); |
| } |
| |
| } // namespace |
| |
| namespace x86 { |
| |
| // Processes enough of the tables to determine how big much memory we need to describe the |
| // descriptor and allocates + fills in the basic information of the descriptor. See CreateDesc for |
| // an explanation of ScopeFunc. |
| template <typename F> |
| zx_status_t IommuDesc::AllocDesc(const ACPI_TABLE_DMAR* table, uint16_t pci_segment, |
| bool whole_segment, F scope_func) { |
| size_t num_scopes = 0; |
| zx_status_t status = scope_func([&num_scopes](const auto& scope) { |
| num_scopes++; |
| return ZX_ERR_NEXT; |
| }); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| size_t num_reserved_mem = 0; |
| size_t num_mem_scopes = 0; |
| |
| status = process_reserved_mem( |
| table, pci_segment, whole_segment, scope_func, |
| [&num_reserved_mem](auto base, auto end) { num_reserved_mem++; }, |
| [&num_mem_scopes](const auto* scope) { num_mem_scopes++; }); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| const size_t reserved_mem_bytes = |
| sizeof(zx_iommu_desc_intel_scope_t) * num_mem_scopes + |
| sizeof(zx_iommu_desc_intel_reserved_memory_t) * num_reserved_mem; |
| const size_t scope_bytes = sizeof(zx_iommu_desc_intel_scope_t) * num_scopes; |
| |
| const size_t desc_bytes = sizeof(zx_iommu_desc_intel_t) + scope_bytes + reserved_mem_bytes; |
| |
| desc_ = fbl::Array(new uint8_t[desc_bytes], desc_bytes); |
| |
| zx_iommu_desc_intel_t* desc = RawDesc(); |
| desc->scope_bytes = static_cast<uint8_t>(scope_bytes); |
| desc->reserved_memory_bytes = static_cast<uint16_t>(reserved_mem_bytes); |
| desc->whole_segment = whole_segment; |
| return ZX_OK; |
| } |
| |
| // Creates descriptor information for the given pci_segment. scope_func is a closure that itself |
| // takes a closure, which is called on every scope. That is scope_func's signature is roughly: |
| // zx_status_t (*scope_func)(zx_status_t(*scope_callback)(zx_iommu_desc_intel_scope_t &scope)) |
| // This provides an abstract way to 'do something for every scope' where finding scopes is itself |
| // something we want to be abstract over. |
| template <typename F> |
| zx_status_t IommuDesc::CreateDesc(const ACPI_TABLE_DMAR* table, uint64_t base, uint16_t pci_segment, |
| bool whole_segment, F scope_func) { |
| zx_status_t status = AllocDesc(table, pci_segment, whole_segment, scope_func); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| zx_iommu_desc_intel_t* desc = RawDesc(); |
| desc->register_base = base; |
| desc->pci_segment = pci_segment; |
| |
| size_t scopes_found = 0; |
| auto scopes = Scopes(); |
| status = scope_func([&scopes, &scopes_found](const auto& scope) { |
| if (scopes_found > scopes.size()) { |
| return ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| scopes[scopes_found] = scope; |
| scopes_found++; |
| return ZX_ERR_NEXT; |
| }); |
| |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| cpp20::span<uint8_t> reserved = ReservedMem(); |
| zx_iommu_desc_intel_reserved_memory_t* last_mem = nullptr; |
| status = process_reserved_mem( |
| table, pci_segment, whole_segment, scope_func, |
| [&last_mem, &reserved](auto start, auto end) { |
| last_mem = reinterpret_cast<zx_iommu_desc_intel_reserved_memory_t*>(reserved.data()); |
| reserved = reserved.subspan(sizeof(*last_mem)); |
| last_mem->base_addr = start; |
| last_mem->len = end - start + 1; |
| last_mem->scope_bytes = 0; |
| }, |
| [&last_mem, &reserved](const auto* scope) { |
| auto* new_scope = reinterpret_cast<zx_iommu_desc_intel_scope_t*>(reserved.data()); |
| reserved = reserved.subspan(sizeof(zx_iommu_desc_intel_scope_t)); |
| memcpy(new_scope, scope, sizeof(zx_iommu_desc_intel_scope_t)); |
| last_mem->scope_bytes = |
| static_cast<uint8_t>(last_mem->scope_bytes + sizeof(zx_iommu_desc_intel_scope_t)); |
| }); |
| |
| return status; |
| } |
| |
| zx_status_t IommuDesc::CreateWholeSegmentDesc(const ACPI_TABLE_DMAR* table, |
| const ACPI_DMAR_HARDWARE_UNIT* unit) { |
| assert(unit->Flags & ACPI_DMAR_INCLUDE_ALL); |
| |
| // The VT-d spec requires that whole-segment hardware units appear in the |
| // DMAR table after all other hardware units on their segment. Search those |
| // entries for scopes to specify as excluded from this descriptor. |
| |
| auto scope_gen = [&unit, &table](auto f) { |
| return for_each_dmar_of_type<ACPI_DMAR_TYPE_HARDWARE_UNIT>(table, [&unit, &f](const auto* rec) { |
| if (rec->Segment != unit->Segment) { |
| return ZX_ERR_NEXT; |
| } |
| zx_status_t status = for_each_scope<ACPI_DMAR_HARDWARE_UNIT>(rec, f); |
| if (status == ZX_OK) { |
| return ZX_ERR_NEXT; |
| } |
| return status; |
| }); |
| }; |
| |
| return CreateDesc(table, unit->Address, unit->Segment, true, scope_gen); |
| } |
| |
| zx_status_t IommuDesc::CreatePartialSegmentDesc(const ACPI_TABLE_DMAR* table, |
| const ACPI_DMAR_HARDWARE_UNIT* unit) { |
| assert((unit->Flags & ACPI_DMAR_INCLUDE_ALL) == 0); |
| |
| auto scope_gen = [&unit](auto f) { return for_each_scope<ACPI_DMAR_HARDWARE_UNIT>(unit, f); }; |
| |
| return CreateDesc(table, unit->Address, unit->Segment, false, scope_gen); |
| } |
| |
| zx_status_t IommuDesc::CreateIommu(const zx::unowned_resource& root_resource) { |
| return zx::iommu::create(*root_resource, ZX_IOMMU_TYPE_INTEL, &Desc(), desc_.size(), &iommu_); |
| } |
| |
| IommuManager::IommuManager(IommuLogger logger) : logger_(std::move(logger)) {} |
| |
| IommuManager::~IommuManager() { |
| if (iommu_mgr == this) { |
| iommu_mgr = nullptr; |
| } |
| } |
| |
| zx_status_t IommuManager::Init(zx::unowned_resource root_resource, bool force_hardware_iommu) { |
| // Prevent double initialization. |
| ZX_DEBUG_ASSERT(!iommu_mgr); |
| iommu_mgr = this; |
| |
| zx_iommu_desc_dummy_t dummy; |
| zx_status_t status = |
| zx::iommu::create(*root_resource, ZX_IOMMU_TYPE_DUMMY, &dummy, sizeof(dummy), &dummy_iommu_); |
| if (status != ZX_OK) { |
| logf(ERROR, "error in zx::iommu::create: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| if (!force_hardware_iommu && !use_hardware_iommu()) { |
| logf(INFO, "not using IOMMU"); |
| return ZX_OK; |
| } |
| |
| ACPI_TABLE_HEADER* table = NULL; |
| ACPI_STATUS acpi_status = AcpiGetTable((char*)ACPI_SIG_DMAR, 1, &table); |
| if (acpi_status != AE_OK) { |
| logf(INFO, "could not find DMAR table"); |
| return ZX_ERR_NOT_FOUND; |
| } |
| ACPI_TABLE_DMAR* dmar = reinterpret_cast<ACPI_TABLE_DMAR*>(table); |
| status = InitDesc(dmar); |
| if (status != ZX_OK) { |
| return status; |
| } |
| for (auto& iommu : iommus_) { |
| status = iommu.CreateIommu(root_resource); |
| if (status != ZX_OK) { |
| logf(ERROR, "acpi-bus: Failed to create iommu object: %s", zx_status_get_string(status)); |
| // Reset the iommus_ so that IommuForBdf doesn't try and use them. |
| iommus_.reset(); |
| return status; |
| } |
| } |
| logf(INFO, "acpi-bus: using IOMMU"); |
| return ZX_OK; |
| } |
| |
| zx_status_t IommuManager::InitDesc(const ACPI_TABLE_DMAR* dmar) { |
| // Count the IOMMUs |
| size_t num_iommus = 0; |
| zx_status_t status = |
| for_each_dmar_of_type<ACPI_DMAR_TYPE_HARDWARE_UNIT>(dmar, [&num_iommus](const auto* r) { |
| num_iommus++; |
| return ZX_ERR_NEXT; |
| }); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| if (num_iommus == 0) { |
| return ZX_OK; |
| } |
| |
| fbl::Array<IommuDesc> iommus = fbl::Array<IommuDesc>(new IommuDesc[num_iommus], num_iommus); |
| |
| size_t iommu_idx = 0; |
| |
| status = for_each_record<ACPI_DMAR_HEADER>(dmar, [this, &dmar, &iommu_idx, |
| &iommus](const ACPI_DMAR_HEADER* record_hdr) { |
| logf(TRACE, "DMAR record: %d", record_hdr->Type); |
| switch (record_hdr->Type) { |
| case ACPI_DMAR_TYPE_HARDWARE_UNIT: { |
| const auto* rec = reinterpret_cast<const ACPI_DMAR_HARDWARE_UNIT*>(record_hdr); |
| |
| logf(TRACE, "DMAR Hardware Unit: %u %#llx %#x", rec->Segment, rec->Address, rec->Flags); |
| const bool whole_segment = rec->Flags & ACPI_DMAR_INCLUDE_ALL; |
| |
| zx_status_t status; |
| if (whole_segment) { |
| status = iommus[iommu_idx].CreateWholeSegmentDesc(dmar, rec); |
| } else { |
| status = iommus[iommu_idx].CreatePartialSegmentDesc(dmar, rec); |
| } |
| if (status != ZX_OK) { |
| logf(ERROR, "acpi-bus: Failed to create iommu desc: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| iommu_idx++; |
| break; |
| } |
| case ACPI_DMAR_TYPE_RESERVED_MEMORY: { |
| ACPI_DMAR_RESERVED_MEMORY* rec = (ACPI_DMAR_RESERVED_MEMORY*)record_hdr; |
| logf(TRACE, "DMAR Reserved Memory: %u %#llx %#llx", rec->Segment, rec->BaseAddress, |
| rec->EndAddress); |
| uintptr_t addr = reinterpret_cast<uintptr_t>(record_hdr); |
| for (uintptr_t scope = addr + 24; scope < addr + rec->Header.Length;) { |
| ACPI_DMAR_DEVICE_SCOPE* s = (ACPI_DMAR_DEVICE_SCOPE*)scope; |
| logf(TRACE, " DMAR Scope: %u, bus %u", s->EntryType, s->Bus); |
| for (size_t i = 0; i < (s->Length - sizeof(*s)) / 2; ++i) { |
| uint16_t v = *(uint16_t*)(scope + sizeof(*s) + 2 * i); |
| logf(TRACE, " Path %ld: %02x.%02x", i, v & 0xffu, (uint16_t)(v >> 8)); |
| } |
| scope += s->Length; |
| } |
| break; |
| } |
| } |
| return ZX_ERR_NEXT; |
| }); |
| |
| if (iommu_idx != num_iommus) { |
| logf(ERROR, "wrong number of IOMMUs found"); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| iommus_ = std::move(iommus); |
| return ZX_OK; |
| } |
| |
| zx::unowned_iommu IommuManager::IommuForPciDevice(uint32_t bdf) { |
| fbl::AutoLock guard{&lock_}; |
| |
| uint8_t bus = (uint8_t)(bdf >> 8); |
| uint8_t dev_func = (uint8_t)bdf; |
| |
| for (auto& iommu : iommus_) { |
| // TODO(teisenbe): Check segments in this function, once we support segments |
| if (iommu.Desc().pci_segment != 0) { |
| continue; |
| } |
| |
| bool found_matching_scope = false; |
| for (auto& scope : iommu.Scopes()) { |
| // TODO(teisenbe): Once we support scopes with multiple hops, need to correct |
| // this routine. |
| // TODO(teisenbe): Once we support bridge entries, need to correct this routine. |
| ZX_DEBUG_ASSERT(scope.num_hops == 1); |
| if (scope.start_bus != bus) { |
| continue; |
| } |
| if (scope.dev_func[0] == dev_func) { |
| found_matching_scope = true; |
| break; |
| } |
| } |
| |
| // Finding a scope has its meaning inverted based on whether this device is whole segment mode. |
| if (iommu.Desc().whole_segment != found_matching_scope) { |
| return iommu.GetIommu(); |
| } |
| } |
| |
| // If there was no match, just use the dummy. |
| return zx::unowned_iommu(dummy_iommu_); |
| } |
| |
| void IommuManager::Logf(fx_log_severity_t severity, const char* file, int line, const char* msg, |
| ...) { |
| va_list args; |
| va_start(args, msg); |
| logger_(severity, file, line, msg, args); |
| va_end(args); |
| } |
| |
| } // namespace x86 |
| |
| zx_status_t iommu_manager_iommu_for_bdf(uint32_t bdf, zx_handle_t* iommu) { |
| ZX_DEBUG_ASSERT(iommu_mgr); |
| *iommu = iommu_mgr->IommuForPciDevice(bdf)->get(); |
| return ZX_OK; |
| } |
| |
| zx_status_t iommu_manager_dummy_iommu(zx_handle_t* iommu) { |
| ZX_DEBUG_ASSERT(iommu_mgr); |
| |
| *iommu = iommu_mgr->DummyIommu()->get(); |
| return ZX_OK; |
| } |