blob: ee45d3da9aa7df1d430360986463e4d18950f310 [file] [log] [blame]
// 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