IOMMU

Introduction

An IOMMU is a device which lies in the interconnect between the CPUs, main memory, and devices, tipically those behind a hub such as a PCIe Root Complex. It is a cornerstone piece for device virtualization, because it allows to configure how devices access memory and generate interrupts.

In systems without an IOMMU, DMA capable devices are able to access system memory and other devices freely. This is a problem if it is desired to expose physical devices to virtual machines, because the guests utilizes guest physical addresses, which are mapped to system physical addresses by the CPU using the nested page tables, but the devices themselves are not aware of this map, so they won’t be accessing to memory correctly.

IOMMUs allow to specify for devices the permissions they have for interacting with main memory, and how such accesses will be translated. Therefore, when a device is passed through to a virtual machine, the memory used by the virtual machine is pinned to memory (that is, it will be mapped from the beginning, and won’t be swapped out, because very few devices support faulting on unmapped pages and doing that from the OS requires special support), and the IOMMU will be given a page table directory with the exact same maps as the nested page tables used by the CPU.

Moreover, IOMMUs allows remapping interrupts. This is mostly of interest in x86, where MSIs are contiguously allocated by devices, and for making them work with X2APICs with IDs greater than 255, although we don’t make use of this mechanism.

x86 IOMMUs

The IOMMUs that can be found in x86 platforms are the Intel IOMMU (Intel VT-d), the AMD IOMMU (AMD-VI), and the virtio IOMMU (virtio-iommu).

In x86 platforms, IOMMUs can also be programmed to perform double-translation for devices. While in passthrough mode, the Device Physical Addresses (DPA) are equal to System Physical Addresses (SPA), and in single-translation mode discussed previously, DPAs are translated to SPAs. However, in double-translation, Device Virtual Addresses (DVA) are translated to DPAs using page tables similar to those of the host CPU, and then DPAs are passed through to SPAs or translated to SPAs using the IOMMU native page tables. While in order to make use of this feature, the device has to be able to issue memory requests upstream using an identifier named the PASID, which selects which double-translation map to use, some IOMMUs (AMD IOMMU) allow to treat the lack of PASIDs (which is the most common case for memory requests) as equal to having a PASID = 0, which allows for some neat tricks (e.g. setting a task’s page directory to a device, or sharing the nested page tables with the device).

x86 IOMMUs also support the concept of interrupt posting, which is useful when passing through a device to a virtual machine. Without interrupt posting,

  1. The device generates an IRQ

  2. The hypervisor handles the IRQ (implies that, if running a guest, it has to exit)

  3. Handle the IRQ, modify the APIC fields, enqueue an IRQ for the virtual CPU

  4. When the virtual CPU is about to execute the guest, inject the interrupt

But, with interrupt posting,

  1. The IOMMU is provided the virtual APIC page

  2. The device generates an IRQ

  3. The IOMMU receives the IRQ, modifies the APIC fields in the virtual APIC page

  4. If the virtual CPU is running, don’t do anything at all, otherwise, write the IRQ in a log

Device isolation

It is important to take into account that the IOMMU can only process upstream requests which pass through it. However, there are two problems:

  1. Devices can interact with each other directly, and those requests never make it to the IOMMU (Peer-to-peer transactions)

  2. Per-device IOMMU configuration requires a device identifier (for IOMMUs that act as Translation Agents for PCIe Root Complexes, this is just the PCI BDF). However, a set of devices can be using the same identifier (e.g. devices behind a PCIe-to-PCI bridge).

Therefore, the operating system cannot work directly with devices, but with groups of IOMMU devices. For simplicity, we assume that in IOMMU groups, both effects occur.

In the case of PCI, devices interact peer-to-peer freely by default, e.g. when a device issues a memory request for another device by targeting its BAR. With the PCIe ACS (Access Control Services) capability, the operating system can discover and configure whether functions of a multi-function can do P2P requests or redirect them upstream, whether root ports must redirect upstream P2P requests from its device to another device, and whether downstream ports must redirect upstream P2P requests between its ports and to other devices, along with other options.

With this, taratos creates the smallest possible IOMMU groups, where P2P between the devices is allowed, and the requests received by the IOMMU might have the device identifier aliased.

When passing through a device to a virtual machine or to a DMA domain (see below), its entire IOMMU group must be considered.

DMA Domains

Most of the times, more than 1 IOMMU group has to be assigned to a virtual machine, or, more generally, it is desired a set of IOMMU groups to share the same memory translations. A DMA domain is such a set.

API

Public facing API

enum IOMMUDMADomainType

An IOMMU DMA domain is a set of IOMMU groups that share the same DMA memory translations.

IOMMU_DMA_DOMAIN_PASSTHROUGH: device physical address == system physical address

IOMMU_DMA_DOMAIN_TRANSLATED: device physical address is translated to a system physical address

IOMMU_DMA_DOMAIN_DOUBLE_TRANSLATED: address of a device memory request that includes a PASID is treated as a device virtual address, will be translated to a device physical address using supplied CPU page tables and then to a system physical address using the IOMMU page tables. If the IOMMU_DMA_DOMAIN_DOUBLE_TRANSLATED_NO_PASID_AS_PASID_0 flag is set, the IOMMU can treat the lack of a PASID as PASID = 0.

Values:

enumerator IOMMU_DMA_DOMAIN_PASSTHROUGH
enumerator IOMMU_DMA_DOMAIN_TRANSLATED
enumerator IOMMU_DMA_DOMAIN_DOUBLE_TRANSLATED
IOMMU_DMA_DOMAIN_PASSTHROUGH_RESTRICTED

(Passthrough domains only) DMA domain is restricted

IOMMU_DMA_DOMAIN_DOUBLE_TRANSLATED_PASSTHROUGH

(Double translated domains only) Passthrough mode, DPA = SPA

IOMMU_DMA_DOMAIN_DOUBLE_TRANSLATED_RESTRICTED

(Double translated domains only) Restricted mode in passthrough (no reads or writes)

IOMMU_DMA_DOMAIN_DOUBLE_TRANSLATED_NO_PASID_AS_PASID_0

(Double translated domains only) Treat no PASID as PASID = 0

struct iommu_dma_domain *iommu_dma_domain_create(enum IOMMUDMADomainType type, unsigned int flags, struct iommu *iommu)

Create a DMA domain.

Use iommu_dma_domain_group_add to associate IOMMU groups with this domain, and iommu_dma_domain_group_del to disassociate them.

Parameters:
  • type – type of the DMA domain

  • flags – flags, depend on the DMA domain

  • iommu – the IOMMU to which to associate the DMA domain

Returns:

A pointer to the DMA domain, or if the IOMMU does not support the - combination, if could not allocate the DMA domain, any error code if iommu_ops.dma_domain_init fails.

int iommu_dma_domain_group_add(struct iommu_dma_domain *dma_domain, struct iommu_group *group)

Add an IOMMU group to an IOMMU domain.

Parameters:
  • dma_domain – the IOMMU DMA domain

  • group – the IOMMU group

Returns:

0 on success, otherwise, error code.

int iommu_dma_domain_group_del(struct iommu_group *group)

Remove an IOMMU group from its IOMMU domain.

Parameters:

group – the IOMMU group

Returns:

0 on success, error code on failure

void iommu_dma_domain_destroy(struct iommu_dma_domain *dma_domain)

Destroy an IOMMU DMA domain.

The caller is responsible for making sure there’s no IOMMU groups in the domain, and that it won’t be used anymore.

Parameters:

dma_domain – the IOMMU DMA domain

int iommu_dma_domain_passthrough_restrict(struct iommu_dma_domain *dma_domain)

(For passthrough domains) Restrict access to memory for devices in the domain.

Parameters:

dma_domain – The DMA domain

Returns:

0 on success, error code on failure

int iommu_dma_domain_passthrough_unrestrict(struct iommu_dma_domain *dma_domain)

(For passthrough domains) Allow access to memory for devices in the domain.

Parameters:

dma_domain – The DMA domain

Returns:

0 on success, error code on failure

int iommu_dma_domain_translated_map(struct iommu_dma_domain *dma_domain, unsigned long long dpstart, paddr_t pstart, bool read, bool write, bool huge)

(For translated domains) Map a range of Device Physical Addresses to a range of System Physical Addresses.

Parameters:
  • dma_domain – The DMA domain

  • dpstart – Start of the Device Physical Address range

  • pstart – Start of the Physical Address range

  • read – Whether reads are allowed

  • write – Whether writes are allowed

  • huge – If true, map a huge page. dpstart and pstart must be huge page aligned. Otherwise, map a regular page.

Returns:

0 on success, error code on failure

int iommu_dma_domain_translated_unmap(struct iommu_dma_domain *dma_domain, unsigned long long dpstart, size_t size)

(For translated domains) Unmap a range of Device Physical Addresses.

The IOMMU’s TLB and the remote IOTLBs, if any, will be flushed for the specified range.

Parameters:
  • dma_domain – The DMA domain

  • dpstart – Start of the Device Physical Address range

  • size – Size of the range

Returns:

0 on success, error code on failure

int iommu_dma_domain_double_translated_map(struct iommu_dma_domain *dma_domain, unsigned long long dpstart, paddr_t pstart, bool read, bool write, bool huge)

(For doubly translated domains) Map a range of Device Physical Addresses to a range of System Physical Addresses.

Parameters:
  • dma_domain – The DMA domain

  • dpstart – Start of the Device Physical Address range

  • pstart – Start of the Physical Address range

  • read – Whether reads are allowed

  • write – Whether writes are allowed

  • huge – If true, map a huge page. dpstart and pstart must be huge page aligned. Otherwise, map a regular page.

Returns:

0 on success, error code on failure

int iommu_dma_domain_double_translated_unmap(struct iommu_dma_domain *dma_domain, unsigned long long dpstart, size_t size)

(For doubly translated domains) Unmap a range of Device Physical Addresses.

The IOMMU’s TLB and the remote IOTLBs, if any, will be flushed for the specified range.

Parameters:
  • dma_domain – The DMA domain

  • dpstart – Start of the Device Physical Address range

  • size – Size of the range

Returns:

0 on success, error code on failure

int iommu_dma_domain_double_translated_passthrough(struct iommu_dma_domain *dma_domain, bool passthrough, bool restricted)

(For doubly translated domains) Set up whether to do passthrough for Device Physical Addresses to System Physical Addresses.

Maps won’t be lost.

Parameters:
  • dma_domain – the IOMMU DMA domain

  • passthrough – Whether to passthrough or not.

  • restricted – If passthrough is true, do not allow reads not writes

Returns:

0 on success, error code on failure

int iommu_dma_domain_double_translated_pasid_page_directory_set(struct iommu_dma_domain *dma_domain, unsigned int pasid, paddr_t page_directory)

(For doubly translated domains) Set a page directory for a given PASID.

Parameters:
  • dma_domain – the IOMMU DMA domain

  • pasid – the PASID

  • page_directory – the page directory root

Returns:

0 on success, error code on failure

int iommu_dma_domain_double_translated_pasid_page_directory_remove(struct iommu_dma_domain *dma_domain, unsigned int pasid)

(For doubly translated domains) Remove a page directory for a given PASID.

Parameters:
  • dma_domain – the IOMMU DMA domain

  • pasid – the PASID

Returns:

0 on success, error code on failure

int iommu_dma_domain_double_translated_flush(struct iommu_dma_domain *dma_domain, unsigned int pasid, unsigned long long dvstart, size_t size, bool invalidate_directories)

(For doubly translated domains) Flush a range of Device Virtual Addresses.

Parameters:
  • dma_domain – the IOMMU DMA domain

  • pasid – the PASID

  • dvstart – start of the DVA range

  • size – size of the DVA range

  • invalidate_directories – whether to invalidate upper-level page tables

Returns:

0 on success, error code on failure

Private API

struct iommu_ops

Public Members

int (*device_check)(struct iommu *iommu, struct device *device)

Check whether the device belongs to the IOMMU.

The implementation can use device#bus to match the device to a bus.

Param iommu:

the IOMMU

Param device:

The device

Return:

0 if match

int (*device_setup)(struct device *device)

Set up a device.

Param device:

the device

Return:

0 on success, error code on failure

int (*group_setup)(struct iommu_group *group)

Set up an IOMMU group.

Param group:

the IOMMU group

Return:

0 on success, error code on failure

bool (*dma_domain_supported)(struct iommu *iommu, enum IOMMUDMADomainType type, unsigned int flags)
Param iommu:

the IOMMU

Param type:

the DMA domain type

Param flags:

the DMA domain flags

Return:

if the DMA domain is supported, otherwise

int (*dma_domain_init)(struct iommu_dma_domain *dma_domain)

Initialize the DMA domain.

The IOMMU implementation must set iommu_dma_domain.ops.

Param dma_domain:

the DMA domain

Return:

0 on success, error code on failure

int (*dma_domain_destroy)(struct iommu_dma_domain *dma_domain)

Destroy the DMA domain.

Param dma_domain:

the DMA domain

struct iommu

Represents an IOMMU.

Public Members

const char *name

Name of the IOMMU.

struct iommu_ops *ops

Operations of the IOMMU.

All must be non-null.

void *private_data

Private data of the IOMMU.

bool initialized

When initialized = true, the IOMMU is ready to set up groups and devices, Otherwise, those will be initialized by the device layer, but their real initialization by their IOMMU driver will be deferred for later.

struct iommu_dma_domain

Public Members

enum IOMMUDMADomainType type

Type of the domain.

unsigned int flags

Flags of the domain.

struct iommu *iommu

IOMMU of the domain.

struct iommu_dma_domain_ops *ops

Operations of the domain.

void *iommu_private_data

Private data for the IOMMU implementation.

struct iommu_dma_domain_ops

IOMMU DMA domain operations.

Public Members

int (*dma_domain_passthrough_group_add)(struct iommu_dma_domain *dma_domain, struct iommu_group *group)

(For passthrough DMA domains) Add the group into the domain.

Param dma_domain:

the IOMMU DMA domain

Param group:

the IOMMU group

Return:

0 on success, error code on failure

int (*dma_domain_passthrough_group_del)(struct iommu_dma_domain *dma_domain, struct iommu_group *group)

(For passthrough DMA domains) Remove the group from the domain.

Param dma_domain:

the IOMMU DMA domain

Param group:

the IOMMU group

Return:

0 on success, error code on failure

int (*dma_domain_passthrough_restrict)(struct iommu_dma_domain *dma_domain)

(For passthrough DMA domains) Disallow reads and writes from and to memory for the devices in the domain.

Param dma_domain:

The DMA domain

Return:

0 on success, error code on failure

int (*dma_domain_passthrough_unrestrict)(struct iommu_dma_domain *dma_domain)

(For passthrough DMA domains) Allow reads and writes from and to memory for the devices in the domain.

Param dma_domain:

The DMA domain

Return:

0 on success, error code on failure

int (*dma_domain_translated_group_add)(struct iommu_dma_domain *dma_domain, struct iommu_group *group)

(For translated DMA domains) Add the group into the domain.

Param dma_domain:

the IOMMU DMA domain

Param group:

the IOMMU group

Return:

0 on success, error code on failure

int (*dma_domain_translated_group_del)(struct iommu_dma_domain *dma_domain, struct iommu_group *group)

(For translated DMA domains) Remove the group from the domain.

Param dma_domain:

the IOMMU DMA domain

Param group:

the IOMMU group

Return:

0 on success, error code on failure

int (*dma_domain_translated_map)(struct iommu_dma_domain *dma_domain, unsigned long long dpstart, paddr_t pstart, bool read, bool write, bool huge)

(For translated DMA domains) Set up a Device Physical Address to System Physical Address translation.

Caller must make sure there is no overlapping with previously mapped ranges.

Param dma_domain:

the IOMMU DMA domain

Param dpstart:

Start of device physical address

Param pstart:

Start of system physical address

Param read:

Page is readable

Param write:

Page is writable

Param huge:

If true, map a huge page, otherwise, map a normal page

Return:

0 on success, error code on failure

int (*dma_domain_translated_unmap)(struct iommu_dma_domain *dma_domain, unsigned long long dpstart, size_t size)

(For translated DMA domains) Remove a Device Physical Address translation range.

Param dma_domain:

the IOMMU DMA domain

Param dpstart:

Start of device physical address

Param size:

Size of the region

Return:

0 on success, error code on failure

int (*dma_domain_double_translated_group_add)(struct iommu_dma_domain *dma_domain, struct iommu_group *group)

(For double translated DMA domains) Add the group into the domain.

Param dma_domain:

the IOMMU DMA domain

Param group:

the IOMMU group

Return:

0 on success, error code on failure

int (*dma_domain_double_translated_group_del)(struct iommu_dma_domain *dma_domain, struct iommu_group *group)

(For double translated DMA domains) Remove the group from the domain.

Param dma_domain:

the IOMMU DMA domain

Param group:

the IOMMU group

Return:

0 on success, error code on failure

int (*dma_domain_double_translated_map)(struct iommu_dma_domain *dma_domain, unsigned long long dpstart, paddr_t pstart, bool read, bool write, bool huge)

(For double translated DMA domains) Set up a Device Physical Address to System Physical Address translation.

Caller must make sure there is no overlapping with previously mapped ranges.

Param dma_domain:

the IOMMU DMA domain

Param dpstart:

Start of device physical address

Param pstart:

Start of system physical address

Param read:

Page is readable

Param write:

Page is writable

Param huge:

If true, map a huge page, otherwise, map a normal page

Return:

0 on success, error code on failure

int (*dma_domain_double_translated_unmap)(struct iommu_dma_domain *dma_domain, unsigned long long dpstart, size_t size)

(For double translated DMA domains) Remove a Device Physical Address translation range.

Param dma_domain:

the IOMMU DMA domain

Param dpstart:

Start of device physical address

Param size:

Size of the region

Return:

0 on success, error code on failure

int (*dma_domain_double_translated_passthrough)(struct iommu_dma_domain *dma_domain, bool passthrough, bool restricted)

(For double translated DMA domains) Set up the passthrough settings.

Param dma_domain:

the IOMMU DMA domain

Param passthrough:

if true, set up DPA = SPA, otherwise, use the maps

Param restricted:

if true and is true, read and write is not allowed, otherwise, both are allowed

Return:

0 on success, error code on failure

int (*dma_domain_double_translated_pasid_page_directory_set)(struct iommu_dma_domain *dma_domain, unsigned int pasid, paddr_t page_directory)

(For double translated DMA domains) Set up a CPU page directory for a PASID.

Param dma_domain:

the IOMMU DMA domain

Param pasid:

the PASID

Param page_directory:

the page directory

Return:

0 on success, error code on failure

int (*dma_domain_double_translated_pasid_page_directory_remove)(struct iommu_dma_domain *dma_domain, unsigned int pasid)

(For double translated DMA domains) Remove a CPU page directory for a pasid.

Param dma_domain:

the IOMMU DMA domain

Param pasid:

the PASID

Return:

0 on success, error code on failure

int (*dma_domain_double_translated_flush)(struct iommu_dma_domain *dma_domain, unsigned int pasid, unsigned long long dvstart, size_t size, bool invalidate_directories)

(For double translated DMA domains) Flush a range of Device Virtual Addresses.

Param dma_domain:

the IOMMU DMA domain

Param pasid:

the PASID

Param dvstart:

start of the DVA range

Param size:

size of the range

Param invalidate_directories:

whether to invalidate directories

Return:

0 on success, error code on failure