blob: 69cf123d81aef48eacd0a1994cf2533c3a0acf88 [file] [log] [blame]
// 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.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) {
const char* value = getenv("driver.iommu.enable");
if (value == NULL) {
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<fbl::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 fbl::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) {
fbl::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;
}
fbl::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 (countof(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;
}
fbl::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::IommuForBdf(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->IommuForBdf(bdf)->get();
return ZX_OK;
}