ArmPkg: Add SMMUv3 SmmuDxe driver with per-StreamID DMA isolation

Add a SMMUv3 DXE driver that implements the EDKII IOMMU protocol to
provide DMA address translation and memory protection through Stage 1
xor Stage 2 translation.

The driver consumes a SMMU_CONFIG HOB containing the platform IORT data,
supports an arbitrary number of SMMUs, and dynamically parses any generic
IORT structure to configure each SMMU accordingly. It installs both the
IOMMU protocol and the IORT ACPI table.

See ArmPkg/Drivers/SmmuDxe/README.md for flow diagrams, the HOB layout,
and platform integration steps.

Signed-off-by: Eeshan Londhe <eeshanlondhe@microsoft.com>
This commit is contained in:
eeshanl 2026-08-11 01:51:55 -07:00
parent 2970e5699b
commit 8c800cd41c
11 changed files with 10258 additions and 0 deletions

View file

@ -98,6 +98,10 @@
gEdkiiTpmEventLogDescHobGuid = { 0x360c4a06, 0x146c, 0x11f0, { 0xb1, 0x73, 0x6b, 0xd5, 0x98, 0xff, 0x54, 0xd6 }}
## SMMU config data
# Include/Guid/SmmuConfig.h
gSmmuConfigHobGuid = { 0xcd56ec8f, 0x75f1, 0x440a, { 0xaa, 0x48, 0x09, 0x58, 0xb1, 0x1c, 0x9a, 0xa7 } }
[Protocols.common]
## Arm System Control and Management Interface(SCMI) Base protocol
## ArmPkg/Include/Protocol/ArmScmiBaseProtocol.h

View file

@ -152,6 +152,7 @@
ArmPkg/Universal/Smbios/OemMiscLibNull/OemMiscLibNull.inf
ArmPkg/Drivers/MmCommunicationPei/MmCommunicationPei.inf
ArmPkg/Drivers/SmmuDxe/SmmuDxe.inf
ArmPkg/Library/FmpDevicePsaFwuLib/FmpDevicePsaFwuLib.inf

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,57 @@
/** @file IoMmu.h
This file is the IoMmu header file for SMMU driver.
Copyright (c) Microsoft Corporation.
SPDX-License-Identifier: BSD-2-Clause-Patent
**/
#pragma once
/**
Page Table bit definitions used by the Smmu/IoMmu for mapping.
See ARM ARM section D8.3.1 (VMSAv8-64 descriptor formats):
<https://developer.arm.com/documentation/ddi0487/mc/>
Note: shared architectural bit definitions (access flag, inner shareable,
AP[2:1], etc.) are pulled from <AArch64/AArch64Mmu.h> (via
<Library/ArmLib.h>) as TT_AF, TT_SH_INNER_SHAREABLE, TT_AP_RW_RW,
TT_AP_RO_RO. Only Stage-2 specific and IOMMU-specific helpers are
defined here.
**/
#define PAGE_TABLE_ENTRY_VALID_BIT 0x1
#define PAGE_TABLE_BLOCK_MASK 0xFFF
#define PAGE_TABLE_DESCRIPTOR (0x1 << 1)
#define PAGE_TABLE_READ_WRITE_FROM_IOMMU_ACCESS(IoMmuAccess) (IoMmuAccess << 6)
#define PAGE_TABLE_WRITE_BIT (0x1 << 7)
#define PAGE_TABLE_S2_MEMATTR_NORMAL_WB (0xF << 2)
// Stage 1 VMSAv8-64 leaf descriptor AttrIndex bits (used when the SMMU
// is configured for Stage 1 translation). AttrIndex 0 selects MAIR[0]
// in the CD (Normal Inner+Outer WBWA per SmmuV3BuildStage1ContextDescriptor).
#define PAGE_TABLE_S1_ATTRINDX0 (0x0 << 2)
typedef UINT64 PAGE_TABLE_ENTRY;
#define PAGE_TABLE_SIZE (EFI_PAGE_SIZE / sizeof(PAGE_TABLE_ENTRY)) // Number of entries in a page table
// Page Table Structure used by SMMU
typedef struct _PAGE_TABLE {
PAGE_TABLE_ENTRY Entries[PAGE_TABLE_SIZE];
} PAGE_TABLE;
/**
Installs the IOMMU Protocol on this SMMU instance.
@retval EFI_SUCCESS All the protocol interface was installed.
@retval EFI_OUT_OF_RESOURCES There was not enough memory in pool to install all the protocols.
@retval EFI_ALREADY_STARTED A Device Path Protocol instance was passed in that is already present in
the handle database.
@retval EFI_INVALID_PARAMETER Handle is NULL.
@retval EFI_INVALID_PARAMETER Protocol is already installed on the handle specified by Handle.
**/
EFI_STATUS
IoMmuInit (
VOID
);

View file

@ -0,0 +1,838 @@
# SmmuDxe Driver
This document describes the System Memory Management Unit (SMMU) driver implementation, and how it integrates with the
DMA capable subsystem. The driver configures the SMMUv3 hardware and implements the IOMMU protocol to provide address
translation and memory protection for DMA operations.
## Architecture Overview
The SmmuDxe driver will consume the SMMU_CONFIG HOB with the IORT data to configure the SMMUs found on the platform.
It will set them up for Stage 2 Translation by default, or Stage 1 Translation when the platform explicitly opts in
via `SMMU_CONFIG->TranslationStage` (see [Translation Stage](#translation-stage)). SmmuDxe will install the IoMmu
Protocol. Translation table mapping can be done by leveraging the IoMmu Protocol. The protocol functions are outlined
below. Seperatley, an IoMmuLib is provided for platforms to use as an abstraction for locating and using the IoMmu
protocol. SmmuDxe will install the IORT ACPI Table. Platform should not install the IORT, but instead pass in the
IORT data with the SMMU_CONFIG HOB.
The system consists of four main components working together:
1. **PCI I/O Protocol**: Provides interface for PCI / NonDiscoverable device access and DMA operations (handle-aware path)
2. **DmaLib (`CoherentDmaLib` / `NonCoherentDmaLib`)**: Handle-less DMA path for firmware-internal agents; forwards
caller-supplied `(IommuBase, DmaId)` to `IoMmuSetAttributeById`
3. **IOMMU Protocol**: Implements DMA remapping and memory protection (`SetAttribute` and `SetAttributeById` entry points)
4. **SMMU Hardware Driver**: Configures and manages the SMMU hardware
## IOMMU Protocol Integration
1. **PCI / NonDiscoverable Driver Initiates DMA** (handle-aware path):
- Device driver calls `PciIo->Map()` / `PciIo->Unmap()`
- SmmuDxe resolves the controller `EFI_HANDLE` → IORT → StreamID(s) + owning SMMU and programs the page table
via `IoMmuSetAttribute`.
2. **Handle-less DMA Agent via DmaLib** (handle-less path):
- Firmware-internal agents (no `EFI_HANDLE`, not in the IORT) call `DmaMap()` / `DmaUnmap()` from
`EmbeddedPkg`'s `CoherentDmaLib` / `NonCoherentDmaLib`:
```c
EFI_STATUS
EFIAPI
DmaMap (
IN DMA_MAP_OPERATION Operation,
IN VOID *HostAddress,
IN OUT UINTN *NumberOfBytes,
IN UINT64 IommuBase, // SmmuV3 base address
IN UINT32 DmaId, // StreamID on Arm SMMU
OUT PHYSICAL_ADDRESS *DeviceAddress,
OUT VOID **Mapping
);
```
- DmaLib calls `IoMmuMap` for bookkeeping, then forwards `(IommuBase, DmaId)` to `IoMmuSetAttributeById`.
3. **IOMMU Protocol Setup**:
- Implements the IOMMU protocol:
```c
struct _EDKII_IOMMU_PROTOCOL {
UINT64 Revision;
EDKII_IOMMU_SET_ATTRIBUTE SetAttribute;
EDKII_IOMMU_MAP Map;
EDKII_IOMMU_UNMAP Unmap;
EDKII_IOMMU_ALLOCATE_BUFFER AllocateBuffer;
EDKII_IOMMU_FREE_BUFFER FreeBuffer;
EDKII_IOMMU_SET_ATTRIBUTE_BY_ID SetAttributeById;
};
```
- SmmuDxe will handle Translation Table initialization
## DMA Mapping with IoMmuLib and IoMmu Protocol
- Maintains up to a 4-level page table, depending on configuration, to map HostAddress and DeviceAddress
- Identity Mapped
1. **IoMmu Map**:
```c
EFI_STATUS
EFIAPI
IoMmuMap (
IN EDKII_IOMMU_OPERATION Operation,
IN VOID *HostAddress,
IN OUT UINTN *NumberOfBytes,
OUT EFI_PHYSICAL_ADDRESS *DeviceAddress,
OUT VOID **Mapping
);
```
- Maps HostAddress to DeviceAddress
- Validates operation type
- Called by PciIo protocol for mapping
### Bounce Buffering
In certain conditions, `IoMmuMap` will allocate a bounce buffer instead of using the original host address directly.
A bounce buffer is a temporary intermediate buffer that is used when the original DMA buffer
cannot be used directly by the device.
**Bounce Buffer Conditions:**
A bounce buffer is allocated when the operation is NOT `EdkiiIoMmuOperationBusMasterCommonBuffer` or
`EdkiiIoMmuOperationBusMasterCommonBuffer64`, AND any of the following conditions are met:
1. **Alignment Requirements Not Met:**
- Alignment check for the start and end of the DMA buffer are needed so that we don't over-map
a buffer in the page table. Page table mappings can only be done page-by-page.
- The `NumberOfBytes` is not 4KB aligned, OR
- The `HostAddress` (PhysicalAddress) is not 4KB aligned
2. **32-bit DMA Limitation:**
- The operation is a 32-bit DMA operation (`EdkiiIoMmuOperationBusMasterRead` or
`EdkiiIoMmuOperationBusMasterWrite`), AND
- Any part of the DMA transfer range (`PhysicalAddress + NumberOfBytes`) exceeds 4GB
**Bounce Buffer Behavior:**
- When a bounce buffer is needed due to **alignment issues only** (with 64-bit operations), memory can be allocated
anywhere in the address space
- When a bounce buffer is needed due to the **32-bit DMA limitation**, memory is allocated below 4GB using
`AllocateMaxAddress` with `DmaMemoryTop` set to `SIZE_4GB - 1`
- The `DeviceAddress` returned points to the bounce buffer, not the original host address
- The `Mapping` handle stores information about both the original host address and the bounce buffer address
**CopyMem on Map (Host → Bounce Buffer):**
A `CopyMem` from the host buffer to the bounce buffer is performed during `IoMmuMap` when:
- A bounce buffer was allocated (NeedRemap is TRUE), AND
- The operation is a **read operation** from the Bus Master's perspective:
- `EdkiiIoMmuOperationBusMasterRead`, OR
- `EdkiiIoMmuOperationBusMasterRead64`
This copy ensures the Bus Master can read the correct data from the bounce buffer during the DMA operation.
For write operations, no copy is needed on Map since the Bus Master will write new data into the bounce buffer.
## DMA Unmapping with IoMmuLib and IoMmu Protocol
1. **PCI Driver Completes DMA**:
- Calls PciIo->Unmap()
- Provides mapping handle
2. **IoMmu Unmap**:
```c
EFI_STATUS
EFIAPI
IoMmuUnmap (
IN VOID *Mapping
);
```
- Only does bounce buffer cleanup.
- Caller must call SetAttributes(0) before IoMmuUnmap to remove R|W attributes to effectivley unmap the page table.
### Bounce Buffer Handling on Unmap
When unmapping a DMA operation that used a bounce buffer (i.e., `DeviceAddress != HostAddress`):
**CopyMem on Unmap (Bounce Buffer → Host):**
A `CopyMem` from the bounce buffer back to the host buffer is performed during `IoMmuUnmap` when:
- A bounce buffer was used (`DeviceAddress != HostAddress`), AND
- The operation is a **write operation** from the Bus Master's perspective:
- `EdkiiIoMmuOperationBusMasterWrite`, OR
- `EdkiiIoMmuOperationBusMasterWrite64`
This copy ensures the processor can access the data that the Bus Master wrote into the bounce buffer.
For read operations, no copy is needed on Unmap since the Bus Master only read data and did not modify it.
**Cleanup:**
- The bounce buffer pages are freed using `FreePages`
- The mapping information structure is freed
For direct mappings (no bounce buffer), only the mapping information structure is freed.
### DMA Access Attributes with IoMmuLib and IoMmu Protocol
1. Setting R/W permissions
- After mapping an address with IoMmuMap()
- Clearning R/W permissions before unmmapping an address with IoMmuUnmap()
- Sets access permissions based on IoMmuAccess type:
- When IoMmuAccess is not 0, R|W permissions are set.
- When IoMmuAccess is 0, no read or write access is permitted.
2. IoMmu SetAttribute
```c
EFI_STATUS
EFIAPI
IoMmuSetAttribute (
IN EFI_HANDLE DeviceHandle,
IN VOID *Mapping,
IN UINT64 IoMmuAccess
);
```
`SetAttribute` is the **worker** of the IOMMU protocol - `IoMmuMap` only allocates the `IOMMU_MAP_INFO`
bookkeeping (and a bounce buffer if needed); it does **not** touch the SMMU page tables. All real page-table
mutation happens here, on the `SetAttribute` call that follows `Map` (and on the `SetAttribute(..., 0)` that
precedes `Unmap`).
The `EFI_HANDLE DeviceHandle` parameter is how SmmuDxe knows *which* device (and therefore which StreamID(s) and
which SMMU) to program:
1. `gBS->HandleProtocol (DeviceHandle, &gEfiPciIoProtocolGuid, ...)` retrieves the `EFI_PCI_IO_PROTOCOL` instance
installed on the handle. Both real PCIe devices (installed by `PciBusDxe`) and non-discoverable devices
(installed by `NonDiscoverablePciDeviceDxe`) expose `PciIo` on the same handle the driver bound to.
2. `PciIo->GetLocation (&Seg, &Bus, &Dev, &Func)` returns the BDF - for non-discoverable devices this is the
synthesized `(0xFF, UniqueId>>5, UniqueId&0x1F, 0)`.
3. `DeviceHandleToStreamId` uses `Seg` to dispatch: real PCIe goes through the IORT Root-Complex ID-mapping path,
Segment `0xFF` goes through the NonDiscoverable lookup table + IORT Named Component path
(see [Per-StreamID Isolation](#per-streamid-isolation)).
4. The resolved primary StreamID's per-stream page-table root and tag (VMID on Stage 2, or ASID inside a
Context Descriptor on Stage 1) are then used to actually install / invalidate the mapping for
`MapInfo->DeviceAddress` with the requested `IoMmuAccess` permissions (or to tear it down when
`IoMmuAccess == 0`).
This is also why the controller handle had to be plumbed through `NonDiscoverablePciDeviceDxe` - its
`PciIoMap`/`PciIoUnmap` now pass `Dev->Handle` to `IoMmuSetAttribute` instead of the old `NULL`, so SmmuDxe can do
the resolution above for non-discoverable devices the same way it does for real PCIe.
3. IoMmu SetAttributeById
```c
EFI_STATUS
EFIAPI
IoMmuSetAttributeById (
IN EDKII_IOMMU_PROTOCOL *This,
IN UINT64 IommuBase,
IN UINT32 DmaId,
IN VOID *Mapping,
IN UINT64 IoMmuAccess
);
```
`SetAttributeById` is for callers that **do not** have an `EFI_HANDLE` for the DMA agent - for example, firmware
internal MMIO/DMA agents that are not described in the IORT and have no UEFI device handle. The caller is
responsible for supplying the owning SMMU/IOMMU base address (`IommuBase`) and the `DmaId` (StreamID on Arm SMMU,
RequesterID on VT-d) emitted by the device. Only that single `DmaId` is programmed; no IORT lookup or alias
resolution is performed.
The protocol now exposes both entry points:
```c
struct _EDKII_IOMMU_PROTOCOL {
UINT64 Revision; // EDKII_IOMMU_PROTOCOL_REVISION == 0x00010001
EDKII_IOMMU_SET_ATTRIBUTE SetAttribute;
EDKII_IOMMU_MAP Map;
EDKII_IOMMU_UNMAP Unmap;
EDKII_IOMMU_ALLOCATE_BUFFER AllocateBuffer;
EDKII_IOMMU_FREE_BUFFER FreeBuffer;
EDKII_IOMMU_SET_ATTRIBUTE_BY_ID SetAttributeById; // Optional; callers must NULL-check.
};
```
`EDKII_IOMMU_PROTOCOL_REVISION` was bumped to `0x00010001` to advertise `SetAttributeById`. Legacy producers that do
not implement `SetAttributeById` leave the field `NULL`; callers (and `IoMmuLib::IoMmuSetAttributeById`) must check
before invoking.
## Per-StreamID Isolation
Each DMA-capable device gets its own per-stream page-table root and per-stream tag. For Stage 2 the tag is a
**VMID** encoded directly in the STE; for Stage 1 the tag is an **ASID** stored in a per-stream Context Descriptor
(CD) whose address the STE's `S1ContextPtr` field points at. Mappings made for one device are **not** visible to
any other device. Conceptually, on every `IoMmuSetAttribute` / `IoMmuSetAttributeById` call the driver:
1. Resolves the caller (device handle, or a caller-supplied `(IommuBase, DmaId)` for the handle-less path) to:
- One or more StreamIDs.
- The base address of the SMMU node that owns them.
For handle-aware callers this resolution walks the platform IORT (and the optional NonDiscoverable lookup table -
see [Non-Discoverable Device Integration](#non-discoverable-device-integration)).
2. Selects the matching SMMU instance by that base address.
3. For the **primary** (first) StreamID, lazily allocates a per-stream page-table root plus:
- **Stage 2:** a VMID, then promotes the Stream Table Entry (STE) from `INVALID` to `STAGE_2_TRANSLATE`
(break-before-make).
- **Stage 1:** a Context Descriptor plus an ASID (the CD's `Ttb0` points at the root, `Asid` holds the tag),
then promotes the STE from `INVALID` to `STAGE_1_TRANSLATE / STAGE_2_BYPASS` with `S1ContextPtr` pointing at
the CD.
4. Aliases every additional StreamID reported for the same device onto the primary's per-stream state:
- **Stage 2:** the alias STE is promoted to carry the same `S2Ttb + S2VMID` as the primary.
- **Stage 1:** the alias STE is promoted to point at the **same CD** as the primary via `S1ContextPtr`, so all
aliases share the same ASID + Stage 1 root.
A single page-table update therefore covers DMA from all StreamIDs that belong to one logical device.
5. Updates the page table with the requested permissions (or tears the mapping down when permissions are cleared).
### Page table root and per-stream tag
Per-stream page tables on the SMMU are tagged by VMID (Stage 2) or ASID (Stage 1). The SMMU caches TLB entries
keyed by that tag, and the STE (Stage 2) or CD (Stage 1) carries the tag field that selects which page tables the
SMMU walks for that stream. Two streams with two different page-table roots **must** have two different tags.
Each page-table root therefore needs its own tag. Each per-stream context gets a unique tag unless the driver
explicitly wants to share that root with another stream (the alias case below):
- **Stage 2**: The STE itself is the single source of truth for the `(Root, Vmid)` binding. On the first mapping
for a StreamID a fresh VMID and root are allocated and the STE is promoted; on subsequent mappings the existing
binding is read back from the live STE's `S2Ttb` / `S2Vmid` fields. VMID `0` is reserved as "unassigned".
- **Stage 1**: The STE + CD together are the source of truth. `S1ContextPtr` locates the CD; the CD's `Ttb0` /
`Asid` fields hold the root pointer and the ASID tag. ASID `0` is reserved as "unassigned". Per SMMUv3 §5.2,
when only Stage 1 is enabled the STE's `S2VMID` field is ignored and Stage 1 TLB entries are tagged with
VMID = 0; TLB invalidation uses `CMD_TLBI_NH_ASID` with VMID = 0.
- Tag width comes from the SMMU's `IDR0.VMID16` (Stage 2) or `IDR0.ASID16` (Stage 1) capability: 8-bit SMMUs use
values `1..0xFF`, 16-bit SMMUs use `1..0xFFFF`. A tag is never reused for a different root within the same boot.
- Aliasing does the inverse: given the **primary's** state, the alias STE is promoted in place to reference the
same state (same `S2Ttb + S2VMID` on Stage 2, same `S1ContextPtr` on Stage 1). If the alias STE already encodes
the same state this is a no-op. That's how multiple StreamIDs share one mapping - they all resolve through the
same tag, so a single TLB invalidation covers every alias.
- On unmap the driver invalidates by VMID (Stage 2) or ASID (Stage 1). Because the tag is unique per page-table
root, that invalidation can't accidentally evict another device's translations.
### End-to-end Flow Diagram
#### Driver Initialization
```mermaid
flowchart TD
Entry[Driver entry point] --> Hob[Read SMMU_CONFIG HOB<br/>gSmmuConfigHobGuid]
Hob --> Ver[Validate config structure version]
Ver --> Loc[Locate ACPI Table + GIC interrupt protocols]
Loc --> Evt[Register ExitBootServices callback]
Evt --> Cfg[Allocate IOMMU state]
Cfg --> Parse[Parse IORT<br/>discover SMMU nodes]
Parse --> SaveIort[Save IORT pointer for<br/>runtime StreamID resolution]
SaveIort --> NcTbl{NonDiscoverable<br/>lookup table<br/>present in HOB?}
NcTbl -- Yes --> NcOn[Save NC device table pointer]
NcTbl -- No --> NcOff[NC StreamID resolution disabled]
NcOn --> Inst
NcOff --> Inst[Install IORT as an ACPI table]
Inst --> Dis[Apply SmmuDisabledList<br/>mark each SMMU enabled/disabled]
Dis --> EnLoop["For each enabled SMMU:<br/>Program SMMU for Stage-2 (default)<br/>or Stage-1 Translation"]
EnLoop --> DisLoop[For each disabled SMMU:<br/>disable translation, set global bypass]
DisLoop --> Install[Install IOMMU protocol]
Install --> Ready([SMMU ready;<br/>STEs sit in INVALID until promoted<br/>by the first mapping call])
```
After init the SMMU hardware is configured but **no device is mapped yet**. Every STE is in `INVALID` until
promoted, so any unsolicited DMA is dropped. STEs are promoted (`STAGE_2_TRANSLATE` by default, or
`STAGE_1_TRANSLATE / STAGE_2_BYPASS` when the platform opted into Stage 1) lazily on the first
`IoMmuSetAttribute` / `IoMmuSetAttributeById` call for that StreamID.
#### Runtime Map / SetAttribute / Unmap
```mermaid
flowchart TD
Drv["PCI / NonDiscoverable<br/>device driver"] -->|PciIo->Map| Map[IoMmuMap<br/>bookkeeping only]
DmaDrv["Handle-less DMA agent<br/>(no EFI_HANDLE)"] -->|DmaMap IommuBase, DmaId| Map
Map -->|allocate mapping info<br/>+ optional bounce buffer| MapDone[(Begin stream configuration<br/>for DMA isolation)]
MapDone -->|PciIo path| SA[IoMmuSetAttribute<br/>DeviceHandle, Mapping, IoMmuAccess]
MapDone -->|DmaLib path| SAX[IoMmuSetAttributeById]
SA --> H[Retrieve PciIo on DeviceHandle]
H --> GL["Query device location<br/>Seg/Bus/Dev/Func"]
GL -->|Seg != 0xFF| RC[IORT Root Complex<br/>RID -> ID mapping -> StreamID + SMMU base]
GL -->|Seg == 0xFF| NC["Reconstruct UniqueId<br/>from synthesized BDF"]
NC --> Tbl[NC lookup table<br/>UniqueId -> ObjectName]
Tbl --> Nc2[IORT Named Component node<br/>ID mappings -> StreamIDs + SMMU base]
RC --> Sel[Find SMMU matching resolved base]
Nc2 --> Sel
SAX -->|caller-supplied SMMU base + StreamId| Sel
Sel --> Pri{Primary StreamID<br/>context exists?}
Pri -- No --> Alloc[Allocate per-stream root<br/>+ assign VMID/ASID<br/>+ CD on Stage 1]
Alloc --> Promote["Promote STE<br/>INVALID -&gt; STAGE_2_TRANSLATE or STAGE_1_TRANSLATE<br/>break-before-make"]
Promote --> Aliases
Pri -- Yes --> Aliases[For each alias StreamID:<br/>bind STE to primary's root + tag<br/><i>Stage 2: shared S2Ttb + VMID<br/>Stage 1: shared CD via S1ContextPtr</i><br/><i>SetAttributeById skips this step</i>]
Aliases --> PT{IoMmuAccess != 0?}
PT -- Yes --> Upd[Update page table<br/>identity-map DeviceAddress<br/>set R/W flags]
PT -- No --> Inv["Invalidate page-table entry<br/>+ TLB invalidate per-VMID (Stage 2)<br/>or per-ASID (Stage 1)"]
Upd --> Done([Return])
Inv --> Done
Done --> Unmap[IoMmuUnmap<br/>preceded by SetAttribute / SetAttributeById with IoMmuAccess=0]
Unmap --> Free["Free bounce buffer if any<br/>Free mapping info<br/>STE/page-table stay until reboot"]
```
The diagram shows three caller paths that converge on the same per-StreamID isolation logic:
- **Map only does bookkeeping.** It allocates the mapping info (and a bounce buffer if needed) and returns a
handle. No STE or page-table is touched. The `DmaLib` wrappers (`CoherentDmaLib` / `NonCoherentDmaLib`) call
straight into `IoMmuMap` and then forward the caller-supplied `(IommuBase, DmaId)` to `IoMmuSetAttributeById`
instead of using a handle.
- **SetAttribute is the worker for handle-aware callers.** It resolves the handle to StreamID(s) and SMMU base via
the IORT, lazily creates a per-stream page-table root and tag (VMID for Stage 2, ASID + CD for Stage 1) and
promotes the STE on first use, aliases additional StreamIDs onto that same state, then either installs
(`IoMmuAccess != 0`) or tears down (`IoMmuAccess == 0`) the identity mapping for the DMA address.
- **SetAttributeById is the handle-less variant** used by `DmaLib` (and any other firmware-internal DMA agent that
isn't described in the IORT). It skips both the handle-to-location lookup and the alias-binding loop - only the
single `(IommuBase, DmaId)` the caller provided is promoted and programmed.
- **Unmap** is purely cleanup of the mapping info and any bounce buffer. The STE and page tables stay promoted for
the lifetime of UEFI; on `ExitBootServices` the SMMU is flipped to global bypass + BME-off so the OS can take
over cleanly.
## Non-Discoverable Device Integration
Non-PCI MMIO devices registered through `MdeModulePkg/Library/NonDiscoverableDeviceRegistrationLib` are bound by
`NonDiscoverablePciDeviceDxe`, which synthesizes a `PciIo` instance over them. The driver no longer auto-assigns a
counter-based BDF; instead the platform supplies a deterministic `UniqueId` at registration time:
```c
EFI_STATUS
EFIAPI
RegisterNonDiscoverableMmioDevice (
IN UINTN UniqueId, // platform-assigned, must be unique
IN NON_DISCOVERABLE_DEVICE_TYPE Type,
IN NON_DISCOVERABLE_DEVICE_DMA_TYPE DmaType,
IN NON_DISCOVERABLE_DEVICE_INIT InitFunc,
IN OUT EFI_HANDLE *Handle OPTIONAL,
IN UINTN NumMmioResources,
...
);
```
The `UniqueId` is stored on the `NON_DISCOVERABLE_DEVICE` protocol and copied into the `NON_DISCOVERABLE_PCI_DEVICE`
instance by `NonDiscoverablePciDeviceDxe`. `PciIo->GetLocation()` then returns a deterministic
`(Segment = 0xFF, Bus = UniqueId >> 5, Device = UniqueId & 0x1F, Function = 0)`. SmmuDxe uses Segment `0xFF` to
detect a non-discoverable device and switch from the PCI Root-Complex lookup path to the Named Component lookup
path.
`NonDiscoverablePciDeviceDxe` also remembers the controller `EFI_HANDLE` on the `NON_DISCOVERABLE_PCI_DEVICE` and
passes it (instead of `NULL`) to `IoMmuSetAttribute` from its `PciIoMap` / `PciIoUnmap` implementations, so SmmuDxe
can resolve the DMA identifier through the IORT.
### Mapping a Non-Discoverable Device to an IORT Named Component
To enable SMMU translation for a non-discoverable device, the platform must:
1. Add a **Named Component** node to the IORT whose `ObjectName` matches the device (e.g. `"USB"`) and whose
ID mappings produce the StreamIDs the device emits. The Named Component's `OutputReference` must point at the
SMMUv3 node that owns those StreamIDs.
2. Publish a **NonDiscoverable lookup table** in the `SMMU_CONFIG` HOB that maps each registered `UniqueId` to the
matching IORT NC `ObjectName`:
```c
typedef struct _SMMU_NC_DEVICE_ENTRY {
UINT64 UniqueId; // Same UniqueId passed to RegisterNonDiscoverableMmioDevice().
CHAR8 ObjName[SMMU_NC_DEVICE_OBJNAME_MAX]; // IORT Named Component ObjectName (NUL-terminated).
} SMMU_NC_DEVICE_ENTRY;
```
Example - one xHCI controller registered with `UniqueId == 1` whose IORT Named Component node is named `"USB"`,
plus an SATA AHCI controller registered with `UniqueId == 2`:
```c
STATIC CONST SMMU_NC_DEVICE_ENTRY NcDeviceTable[] = {
// { UniqueId, IORT NC ObjectName }
{ 0x1, "USB" },
{ 0x2, "SATA_AHCI" },
};
```
Each entry's `UniqueId` is exactly the value the platform passed to `RegisterNonDiscoverableMmioDevice`, and the
`ObjName` must character-for-character match the `ObjectName` field of an `EFI_ACPI_IORT_TYPE_NAMED_COMP` node in
the IORT blob. SmmuDxe uses this two-step lookup (UniqueId → ObjectName → IORT NC node → StreamID list +
owning SMMU base) every time a non-discoverable device calls `IoMmuSetAttribute`.
3. Point the new `NcDeviceListSize`/`NcDeviceListOffset` fields in `SMMU_CONFIG` at that table (see
[SMMU_CONFIG HOB Layout](#smmu_config-hob-layout)).
At translation time, when `PciIo->GetLocation()` returns Segment `0xFF`, SmmuDxe reconstructs the original
`UniqueId` from `(Bus << 5) | (Device & 0x1F)`, looks it up in the NC table to get an `ObjectName`, walks the IORT
for a Named Component node with that name, and **uses every StreamID in that node's ID mappings** - not just one.
This is where the alias logic from [Per-StreamID Isolation](#per-streamid-isolation) begins: the first StreamID
returned becomes the **primary** and gets its own per-stream page-table root and tag (a VMID on Stage 2, or an
ASID inside a Context Descriptor on Stage 1). Every other StreamID emitted by the same IORT NC node is then
**aliased** so its STE points at the *same* root and uses the *same* tag - on Stage 2 the alias STE carries the
same `S2Ttb + S2VMID` as the primary, on Stage 1 the alias STE's `S1ContextPtr` points at the primary's CD, so
all aliases share the same ASID + Stage 1 root. The device therefore sees one unified mapping no matter which of
its StreamIDs the transaction was tagged with.
```text
NC HOB IORT Named Component SMMU per-stream state
+-----------+ match +---------------------+ ID maps +-------------------------------------------+
| UniqueId | --------> | ObjectName = "USB" | ----------> | Primary SID -> Root R, tag T
| ObjName | | - StreamID 0x10 | | Alias SID -> Root R, tag T (shared)
+-----------+ | - StreamID 0x11 | | Alias SID -> Root R, tag T (shared)
+---------------------+ +-------------------------------------------+
tag = VMID (Stage 2, in STE)
= ASID (Stage 1, in shared CD via S1ContextPtr)
```
Adding or removing alias StreamIDs is therefore purely an IORT edit - the platform NC table stays small and only
needs one entry per logical device.
## SMMU_CONFIG HOB Layout
The platform builds one HOB (`gSmmuConfigHobGuid`) containing the `SMMU_CONFIG` header followed (in any order) by the
IORT blob, the optional SMMU disable list, and the optional NonDiscoverable device lookup table. Each section is
located via its offset and size in the header:
```c
typedef struct _SMMU_CONFIG {
UINT32 VersionMajor;
UINT32 VersionMinor;
UINT32 SmmuDisabledListSize; // Size of SmmuDisabledList in bytes.
UINT32 SmmuDisabledListOffset; // Offset to the SmmuDisabledList from the start of the HOB.
UINT32 IortSize;
UINT32 IortOffset; // Offset to the IORT table from the start of the HOB.
UINT32 NcDeviceListSize; // Size of the NonDiscoverable lookup array, in bytes.
UINT32 NcDeviceListOffset; // Offset to the NonDiscoverable lookup array. 0 if absent.
SMMU_TRANSLATION_STAGE TranslationStage; // SmmuTranslationStage2 (default) or SmmuTranslationStage1.
// Applies to every SMMU described by this HOB.
} SMMU_CONFIG;
```
`SMMU_TRANSLATION_STAGE` is a shared enum defined in `<Guid/SmmuConfig.h>`:
```c
typedef enum _SMMU_TRANSLATION_STAGE {
SmmuTranslationStage2 = 0, // Stage 2 translate, Stage 1 bypass (default)
SmmuTranslationStage1 = 1, // Stage 1 translate, Stage 2 bypass
} SMMU_TRANSLATION_STAGE;
```
Any value other than `SmmuTranslationStage1` is coerced to `SmmuTranslationStage2` by SmmuDxe.
Visual layout (one possible ordering):
```text
+----------------------------+
| SMMU_CONFIG | header
+----------------------------+ <-- IortOffset
| IORT table data | IortSize bytes
+----------------------------+ <-- NcDeviceListOffset
| SMMU_NC_DEVICE_ENTRY[ N ] | NcDeviceListSize bytes (multiple of sizeof(SMMU_NC_DEVICE_ENTRY))
+----------------------------+ <-- SmmuDisabledListOffset (optional)
| UINT64 disabledBases[] | SmmuDisabledListSize bytes
+----------------------------+
```
## SMMU Configuration
### 1. SMMUv3 Hardware Setup
The SMMU is configured in **Stage 2 translation** mode by default, or in **Stage 1 translation** mode when the
platform sets `SMMU_CONFIG->TranslationStage = SmmuTranslationStage1`. Every SMMU described by the HOB is
configured for the same stage (see [Translation Stage](#translation-stage)). Both modes bring up:
- Stream table for device ID mapping
- Command queue for SMMU operations, like TLB management
- Event queue for error handling
- 4KB translation granule
Stage 1 additionally allocates a per-stream Context Descriptor (CD) whose `S1ContextPtr` is installed in the STE
so the SMMU can locate the CD (and through it, the Stage 1 root and ASID) when translating that stream.
### Translation Stage
- **Stage 2 (default, `SmmuTranslationStage2`)**: The SMMU installs `STAGE_2_TRANSLATE / STAGE_1_BYPASS` STEs.
Requires `IDR0.S2p`. Each StreamID's page-table root is encoded directly in the STE (`S2Ttb`, `S2VMID`).
- **Stage 1 (`SmmuTranslationStage1`)**: The SMMU installs `STAGE_1_TRANSLATE / STAGE_2_BYPASS` STEs. Requires
`IDR0.S1p`. Each StreamID's page-table root and ASID live in a Context Descriptor (64-byte, page-aligned); the
STE's `S1ContextPtr` points at the CD. Stage 1 TLB entries are tagged with VMID = 0 (SMMUv3 §5.2) and
invalidation uses `CMD_TLBI_NH_ASID`. Concatenation at the starting level is not architecturally allowed for
Stage 1, so the walker uses a single-page root and dynamically picks L0 (OAS > 39 b) or L1 (OAS ≤ 39 b) as the
starting level.
Choice of stage is a policy decision between platforms - the identity-mapping semantics that DMA agents observe
are identical.
### 2. Page Table Structure
The IOMMU uses up to a 4-level page table structure for DMA address translation:
<https://developer.arm.com/documentation/101811/0104/Translation-granule/The-starting-level-of-address-translation>
Depending on configuration from the SMMU registers, the starting level of translation is chosen. Stage 2 supports
Concatenated Translation Tables at the starting level for wide OAS (Arm ARM D8.2.2); Stage 1 is architecturally
forbidden from concatenating at the starting level (Arm ARM D8.5.2), so on Stage 1 the walker always installs a
single-page root and picks the starting level dynamically:
- **Stage 2 (default)**: 16-page concatenated root; L1 start for OAS < 44 b, L0 start for OAS 44 b.
- **Stage 1**: single-page root; L1 start for OAS ≤ 39 b, L0 start otherwise. Full 48-bit input is covered by a
single L0 page (512 entries × 512 GB).
### 3. Address Translation Process
1. **Device Issues DMA**:
- Device uses IOVA (I/O Virtual Address)
- SMMU intercepts access
2. **SMMU Translation**:
- Looks up Stream Table Entry (STE)
- 2 level or Linear Stream Table. Depending on configurable maximum StreamId via IORT.
- Stage 2: walks up to 4-level Stage 2 page tables directly from `STE.S2Ttb`.
- Stage 1: dereferences `STE.S1ContextPtr` to fetch the Context Descriptor, then walks up to 4-level Stage 1
page tables from `CD.Ttb0`.
- Converts IOVA to PA (Physical Address)
### Leaf Page Table Entry Updates and Break-Before-Make
ARM's break-before-make (BBM) rule applies when a *live* leaf entry's PA / attributes change in place - without an
Invalid+TLBI step in between, a concurrent walk can observe a torn entry. `UpdateMapping` (`IoMmu.c`) skips BBM
because the only transitions it performs on a leaf are **Invalid ↔ Valid**; PA and flags are never rewritten on a
Valid entry.
That invariant is enforced on the map path: if the leaf is already valid, the new `Entry` must be byte-for-byte
identical, otherwise `EFI_DEVICE_ERROR` is returned instead of overwriting in place.
### TLB Invalidation
The SMMU TLB caches translations tagged by the per-stream tag (VMID for Stage 2, ASID for Stage 1). The driver
therefore issues a different invalidation command depending on the configured translation stage.
| Stage | Helper | Command | Notes |
|---------|-------------------------------|------------------------------------------------------|-------|
| Stage 2 | `SmmuV3TLBInvalidateAllStage2` | `CMD_TLBI_S12_VMALL(Vmid)` | Invalidates **all** Stage 1 + Stage 2 TLB entries owned by the given `VMID`. VMID is read from the live STE (`S2Vmid`). VMID `0` is reserved and rejected. |
| Stage 1 | `SmmuV3TLBInvalidateAllStage1` | `CMD_TLBI_NH_ASID(VMID = 0, Asid)` | The invalidation scope is equivalent to that of ASIDE1: when issued from the Non-secure Command queue, invalidates stage 1 NS-EL1 non-global entries by ASID and VMID. When only Stage 1 is enabled the STE's `S2VMID` is ignored and Stage 1 TLB entries are tagged with `VMID = 0`, so the invalidation always targets `VMID = 0`. ASID `0` is reserved and rejected. |
Because per-stream tags (VMID/ASID) are unique per page-table root within the boot
(see [Page table root and per-stream tag](#page-table-root-and-per-stream-tag)), an invalidation for one device
cannot evict another device's translations even though the command targets an entire tag rather than a single IPA.
## Memory Protection
The IOMMU protocol provides several protection mechanisms:
1. **Access Control**:
- Read/Write permissions per mapping
- Device isolation through Stream IDs
2. **Address Range Protection**:
- Validates DMA addresses
- Prevents access outside mapped regions
3. **Error Handling**:
- Translation faults logged to Event Queue
- See [SMMU Fault Reporting via GIC Interrupts](#smmu-fault-reporting-via-gic-interrupts) for how faults are
surfaced at runtime.
### SMMU Fault Reporting via GIC Interrupts
SmmuDxe hooks the SMMU's two error-reporting interrupts so translation faults and command/global errors are surfaced
the moment they happen instead of being noticed only on the next mapping call:
- **EVTQ IRQ** - fires when the SMMU pushes an entry into its **Event Queue** (e.g. a Stage 1 or Stage 2
translation fault for some StreamID, a forbidden access, a CD/STE fetch fault, etc.). Each SMMU carries the
per-SMMU `EvtqIrqNum` parsed from the IORT SMMUv3 node.
- **GERR IRQ** - fires on **Global Errors** reported through `GERROR` / `GERRORN` (command-queue consumer faults,
MSI write errors, queue overflow, etc.). Tracked as `GerrIrqNum`.
#### Configuration path
1. During driver init the SMMU driver locates the platform's GIC interrupt producer (the EDK II
`HardwareInterrupt2` protocol) and caches it. If the protocol isn't present, IRQ hookup is skipped
(non-fatal) - faults will only be surfaced on the next mapping call.
2. For each enabled SMMU, both the EVTQ IRQ and the GERR IRQ are registered with a shared ISR: the source is bound
to the handler, configured as edge-rising, and enabled at the GIC.
3. The SMMU itself is programmed (in the same configure path) to deliver these IRQs as wired SPIs through the GIC.
MSI delivery is not used here - the IORT-supplied SPI numbers are routed directly.
#### Runtime handling
The shared ISR runs at GIC dispatch:
1. Walks the driver's SMMU list to find the enabled SMMU whose EVTQ IRQ or GERR IRQ matches the firing source.
2. Drains and pretty-prints every queued Event Queue entry plus the latched `GERROR` bits, then asserts so the
failure is loud during development.
3. Always signals end-of-interrupt to the GIC to deassert the IRQ.
The same error-logging is also invoked opportunistically from `IoMmuSetAttribute` / `IoMmuSetAttributeById` so
anything that slipped past the IRQ path (or was raised while interrupts were masked) is still surfaced on the next
mapping call.
## Continuous Protection Through the OS
### SMMU Initialization Pre-DXE
To ensure continuous protection, the SMMU should be set in abort mode during the pre-DXE phase. This means that the SMMU
will block all transactions unless explicitly configured to allow them. This setup ensures that no unauthorized DMA
operations can occur before the SMMU is fully configured.
### Interaction with PcdDisableBMEonEBS
The PcdDisableBMEonEBS (Disable Bus Master Enable on Exit Boot Services) setting plays a crucial role in maintaining
system security during the transition from firmware to the operating system.
The following steps outline the end-to-end interaction:
1. Pre-DXE Initialization:
The SMMU is set to abort mode to block all transactions. This ensures that no DMA operations can bypass the SMMU's
protection mechanisms.
2. DMA Remapping with SMMU:
The SMMU is configured to translate addresses and manage memory protection.
The SMMU's page tables and stream tables are set up to allow only authorized DMA operations.
3. Exit Boot Services (EBS):
`SmmuV3ExitBootServices` runs at `TPL_NOTIFY` for every enabled SMMU and decides per-SMMU between two terminal
states based on `SMMU_INFO->EBSBehaviorAbort`:
- **Default - `SmmuV3GlobalAbort`.** The SMMU is flipped into global-abort: every StreamID, including those that
were translated during UEFI, has its transactions aborted. Translation is then disabled
(`SmmuV3DisableTranslation`). This is the safe default: nothing can DMA until the OS reprograms the SMMU.
- **Opt-out for SMMUs with RMR-mapped streams - `SmmuV3SetGlobalBypass`.** If any IORT RMR range was pre-mapped
under this SMMU during init (`EBSBehaviorAbort == FALSE`), the SMMU is set to global-bypass instead so the
reserved-memory devices the firmware handed off to the OS (UEFI runtime framebuffer, etc.) keep working until
the OS rebinds them. Translation is still disabled. **Other SMMUs without RMRs are unaffected - their
StreamIDs are still protected** until the OS reconfigures them.
- In addition, `PcdDisableBMEonEBS` controls clearing the PCI Bus Master Enable bit on PCI controllers, which
prevents any PCIe device from initiating new DMA in the abort window.
The net effect: SMMUs that must keep RMR streams alive go to bypass; every other SMMU stays in abort. There is no
point in the handoff where a previously-protected StreamID can DMA freely against memory it didn't have a mapping
for during UEFI.
4. OS Reconfiguration:
The operating system reconfigures the SMMU as needed for its own DMA remapping and memory protection requirements.
By following these steps, the system ensures that the SMMU provides continuous protection from the pre-DXE phase
through to the operating system's reconfiguration. This approach helps prevent unauthorized DMA operations and
maintains system security.
## Limitations
Current implementation constraints:
1. Supports cache coherent SMMUs only
2. Fixed 4KB granule size
3. 48-bit address space limit
4. Single translation stage per HOB
- The platform picks Stage 2 (default) or Stage 1 via `SMMU_CONFIG->TranslationStage`, and every SMMU described
by the HOB is configured for the same stage. Nested Stage 1 + Stage 2 is not supported.
- By only ever programming one stage, firmware hands off a translation regime that both a hypervisor and a
bare-metal OS can re-use for live handoff scenarios.
5. Only Identity-mapped page tables are supported (one per StreamID, allocated lazily on first
`IoMmuSetAttribute` for that StreamID)
## Future Enhancements
Potential improvements:
1. Multiple translation granule support
2. Different page table mapping schemes besides Identiy-Mapping
3. Updated IoMmu Protocol to optimize redundancies
4. Bounce Buffer Optimization
- Remove the `NumberOfBytes` end address alignment check in `IoMmuMap`
- Update individual drivers to allocate their DMA buffers by page (page-aligned and page-sized)
- This eliminates one case for bounce buffering, improving performance by avoiding unnecessary
buffer copies and allocations when only the end address is unaligned
## Configuration Options
Key SMMU settings controlled through the SMMU config HOB:
- **IORT data:** The complete IORT table data that the SMMU(s) will be configured with. Located by
`IortOffset` / `IortSize`. SmmuDxe walks this both at init (to discover SMMU nodes / RMR ranges) and at runtime
(to resolve `DeviceHandle` → StreamID for `IoMmuSetAttribute`).
- **SmmuDisabledList:** Provides the platform the ability to individually disable/bypass an SMMU if needed.
This list is a `UINT64[]` of SMMU base addresses that the platform wants to disable/bypass, located by
`SmmuDisabledListOffset` / `SmmuDisabledListSize`. By default every SMMU found in the IORT is configured for the
translation stage picked by `TranslationStage` (Stage 2 by default); any SMMU whose base appears in this list is
instead set to global-bypass.
- **NonDiscoverable device lookup table (`NcDeviceListOffset` / `NcDeviceListSize`):** Maps each platform-assigned
`UniqueId` (the value passed to `RegisterNonDiscoverableMmioDevice`) to the matching IORT Named Component
`ObjectName`. SmmuDxe uses this two-step lookup
(`UniqueId → ObjectName → IORT NC node → StreamID(s) + owning SMMU base`) every time a non-discoverable device
calls `IoMmuSetAttribute`. Omit the table (or leave the offset/size as `0`) only if the platform has no
non-discoverable DMA-capable devices; otherwise NC StreamID resolution will fail. See
[Mapping a Non-Discoverable Device to an IORT Named Component](#mapping-a-non-discoverable-device-to-an-iort-named-component)
for the entry format and an example.
- **TranslationStage:** Chooses the translation stage every SMMU in the HOB is configured for. Default
`SmmuTranslationStage2` matches previous behavior (Stage 2 translate, Stage 1 bypass);
`SmmuTranslationStage1` opts into Stage 1 translation (Stage 2 bypass). Any other value is coerced to
`SmmuTranslationStage2`. Requires the SMMU to advertise `IDR0.S1p` when Stage 1 is selected; otherwise
`SmmuV3Configure` will return `EFI_UNSUPPORTED` and driver init will fail. See
[Translation Stage](#translation-stage) for behavior differences.
## Platform Integration Instructions
Generic Platform Integration:
1. **Build the IORT.** Include one SMMUv3 node, the necessary ITS / Root Complex nodes for PCI, and one
`EFI_ACPI_IORT_TYPE_NAMED_COMP` node per non-discoverable DMA-capable device. Each Named Component's ID mappings
must produce the StreamIDs the device emits and reference the owning SMMUv3 node.
2. **Register non-discoverable devices with a stable `UniqueId`** using `RegisterNonDiscoverableMmioDevice`. The same
`UniqueId` is what SmmuDxe will reconstruct from `PciIo->GetLocation()` later, so it must be unique across all
registrations on the platform.
3. **Publish the SMMU_CONFIG HOB** (`gSmmuConfigHobGuid`). Append (in any order) the IORT blob, the NC device lookup
table (see [Non-Discoverable Device Integration](#non-discoverable-device-integration)), and the optional SMMU
disable list, then point the offsets/sizes in the `SMMU_CONFIG` header at them. Leave `TranslationStage` at
`SmmuTranslationStage2` (default) unless the platform explicitly wants Stage 1 - see
[Translation Stage](#translation-stage). Example:
```c
STATIC CONST SMMU_NC_DEVICE_ENTRY NcDeviceTable[] = {
{ /* UniqueId */ 0x1, /* IORT NC ObjectName */ "USB" },
};
SmmuConfig = AllocateZeroPool (sizeof (SMMU_CONFIG) + sizeof (IortData) + sizeof (NcDeviceTable));
SmmuConfig->VersionMajor = CURRENT_SMMU_CONFIG_VERSION_MAJOR;
SmmuConfig->VersionMinor = CURRENT_SMMU_CONFIG_VERSION_MINOR;
SmmuConfig->IortSize = sizeof (IortData);
SmmuConfig->IortOffset = sizeof (SMMU_CONFIG);
SmmuConfig->NcDeviceListSize = sizeof (NcDeviceTable);
SmmuConfig->NcDeviceListOffset = sizeof (SMMU_CONFIG) + sizeof (IortData);
SmmuConfig->TranslationStage = SmmuTranslationStage2; // or SmmuTranslationStage1 to opt into Stage 1
CopyMem ((UINT8 *)SmmuConfig + SmmuConfig->IortOffset, &IortData, sizeof (IortData));
CopyMem ((UINT8 *)SmmuConfig + SmmuConfig->NcDeviceListOffset, NcDeviceTable, sizeof (NcDeviceTable));
BuildGuidDataHob (&gSmmuConfigHobGuid, SmmuConfig, sizeof (SMMU_CONFIG) + sizeof (IortData) + sizeof (NcDeviceTable));
```
4. **Do not install the IORT yourself** - SmmuDxe installs it as an ACPI table after parsing.
5. **Disable specific SMMUs (optional).** Append a `UINT64[]` of SMMU base addresses to put in bypass mode and point
`SmmuDisabledListOffset` / `SmmuDisabledListSize` at it.
6. **Handle-less DMA agents (optional).** Firmware components that have no UEFI device handle (and therefore are not
in the IORT) can program the SMMU directly through `IoMmuLib::IoMmuSetAttributeById (IommuBase, DmaId, ...)`. The
caller is responsible for supplying the correct SMMU base and DMA identifier.
Integration with Qemu:
- SMMU is supported on Qemu v9.1.50 and above.
## Relevant Docs
- SMMUv3 specification <https://developer.arm.com/documentation/ihi0070/latest/>
- Useful ARM SMMU documentation - <https://developer.arm.com/documentation/109242/0100/Programming-the-SMMU>
- Arm AArch64 memory management guide - <https://developer.arm.com/documentation/101811/0104>
- ARM a_a-profile_architecture_reference_manual <https://developer.arm.com/documentation/ddi0487/mc/>
- Intel IOMMU for DMA protection in UEFI <https://www.intel.com/content/dam/develop/external/us/en/documents/intel-whitepaper-using-iommu-for-dma-protection-in-uefi.pdf>
- IORT documentation <https://developer.arm.com/documentation/den0049/latest/>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
## @file SmmuDxe.inf
# This driver initializes the SmmuV3 hardware to enable stage 2 translation and DMA remapping.
# The IOMMU protocol is implemented assuming a 4-level Page Table Structure.
#
# For more info see README.md
#
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = SmmuDxe
FILE_GUID = BE506866-85F2-45EC-AC01-9BBF39D4CA78
MODULE_TYPE = DXE_DRIVER
VERSION_STRING = 1.0
ENTRY_POINT = InitializeSmmuDxe
[Sources]
IoMmu.c
IoMmu.h
SmmuDxe.c
SmmuV3.h
SmmuV3Util.c
[Guids]
gSmmuConfigHobGuid
gEfiEventExitBootServicesGuid
[Packages]
ArmPkg/ArmPkg.dec
EmbeddedPkg/EmbeddedPkg.dec
MdeModulePkg/MdeModulePkg.dec
MdePkg/MdePkg.dec
[LibraryClasses]
ArmLib
BaseLib
BaseMemoryLib
IoLib
HobLib
MemoryAllocationLib
TimerLib
UefiDriverEntryPoint
[Protocols]
gEdkiiIoMmuProtocolGuid ## PRODUCES
gEfiAcpiTableProtocolGuid ## CONSUMES
gEfiPciIoProtocolGuid ## CONSUMES
gHardwareInterrupt2ProtocolGuid ## CONSUMES
[Depex]
gHardwareInterrupt2ProtocolGuid

View file

@ -0,0 +1,981 @@
/** @file SmmuV3.h
This file is the SmmuV3 header file for SMMU driver compliant with the Smmu spec:
<https://developer.arm.com/documentation/ihi0070/latest/>
Copyright (c) Microsoft Corporation.
SPDX-License-Identifier: BSD-2-Clause-Patent
**/
#pragma once
#include <Register/SmmuV3Registers.h>
#include <Uefi/UefiBaseType.h>
#include <IndustryStandard/IoRemappingTable.h>
#include <Protocol/IoMmu.h>
#include <Protocol/HardwareInterrupt2.h>
#include <Guid/SmmuConfig.h>
#include "IoMmu.h"
// Number of levels in the page table
#define PAGE_TABLE_DEPTH 4
// If the starting level is 1, and the address width exceeds 39 bits, then the page table is concatenated
#define PAGE_TABLE_CONCATENATED_PAGES_BITS_CUTOFF 39
// Page Table Index macro to calculate the index of the page table entry based on the level and address width, supports concatenated page tables
#define PAGE_TABLE_INDEX(VirtualAddress, Level, OutputAddressWidth, TranslationStartingLevel, PageTableRootConcatenated) \
(PageTableRootConcatenated && (Level == 1) && (TranslationStartingLevel == Level)) ? \
(((VirtualAddress) >> (12 + (9 * ((PAGE_TABLE_DEPTH - 1) - (Level))))) & \
((1 << (9 + ((OutputAddressWidth) - PAGE_TABLE_CONCATENATED_PAGES_BITS_CUTOFF))) - 1)) : \
(((VirtualAddress) >> (12 + (9 * ((PAGE_TABLE_DEPTH - 1) - (Level))))) & 0x1FF)
#define PAGE_TABLE_4_LEVEL_OUTPUT_ADDRESS_WIDTH_MIN 44
#define PAGE_TABLE_OUTPUT_ADDRESS_WIDTH_MAX 48
#define PAGE_TABLE_OUTPUT_ADDRESS_WIDTH_MIN 32
#define PAGE_TABLE_ROOT_CONCATENATED_PAGES_MAX 16
#define PAGE_TABLE_ROOT_STAGE1_PAGES 1
#define PAGE_TABLE_ROOT_STAGE2_PAGES PAGE_TABLE_ROOT_CONCATENATED_PAGES_MAX
#define PAGE_TABLE_ROOT_PAGES(SmmuInfo) \
(((SmmuInfo)->TranslationStage == SmmuTranslationStage1) \
? PAGE_TABLE_ROOT_STAGE1_PAGES \
: PAGE_TABLE_ROOT_STAGE2_PAGES)
// Cacheability and Shareability attributes
#define ARM64_RGNCACHEATTR_NONCACHEABLE 0
#define ARM64_RGNCACHEATTR_WRITEBACK_WRITEALLOCATE 1
#define ARM64_RGNCACHEATTR_WRITETHROUGH 2
#define ARM64_RGNCACHEATTR_WRITEBACK_NOWRITEALLOCATE 3
#define ARM64_SHATTR_NON_SHAREABLE 0
#define ARM64_SHATTR_OUTER_SHAREABLE 2
#define ARM64_SHATTR_INNER_SHAREABLE 3
#define SMMUV3_PAGE_1_OFFSET 0x10000
// log2 size of the command queue
#define SMMUV3_COMMAND_QUEUE_LOG2ENTRIES (8)
//
// Define the size of each entry in the command queue.
//
#define SMMUV3_COMMAND_QUEUE_ENTRY_SIZE (sizeof(SMMUV3_CMD_GENERIC))
//
// Macros to compute command queue size given its Log2 size.
//
#define SMMUV3_COMMAND_QUEUE_SIZE_FROM_LOG2(QueueLog2Size) \
((UINT32)(1UL << (QueueLog2Size)) * \
(UINT16)(SMMUV3_COMMAND_QUEUE_ENTRY_SIZE))
// log2 size of the event queue
#define SMMUV3_EVENT_QUEUE_LOG2ENTRIES (7)
//
// Define the size of each entry in the event queue.
//
#define SMMUV3_EVENT_QUEUE_ENTRY_SIZE (sizeof(SMMUV3_FAULT_RECORD))
//
// Macros to compute event queue size given its Log2 size.
//
#define SMMUV3_EVENT_QUEUE_SIZE_FROM_LOG2(QueueLog2Size) \
((UINT32)(1UL << (QueueLog2Size)) * (UINT16)(SMMUV3_EVENT_QUEUE_ENTRY_SIZE))
#define SMMUV3_COUNT_FROM_LOG2(Log2Size) (1UL << (Log2Size))
//
// Macro to determine if a queue is empty. It is empty if the producer and
// consumer indices are equal and their wrap bits are also equal.
//
#define SMMUV3_IS_QUEUE_EMPTY(ProducerIndex, \
ProducerWrap, \
ConsumerIndex, \
ConsumerWrap) \
\
(((ProducerIndex) == (ConsumerIndex)) && ((ProducerWrap) == (ConsumerWrap)))
//
// Macro to determine if a queue is full. It is full if the producer and
// consumer indices are equal and their wrap bits are different.
//
#define SMMUV3_IS_QUEUE_FULL(ProducerIndex, \
ProducerWrap, \
ConsumerIndex, \
ConsumerWrap) \
\
(((ProducerIndex) == (ConsumerIndex)) && ((ProducerWrap) != (ConsumerWrap)))
//
// SMMUV3 Stream Table Entry bit definitions
//
#define SMMUV3_STREAM_TABLE_ENTRY_CCA 1 // Cache Coherent Attribute
#define SMMUV3_STREAM_TABLE_ENTRY_CPM 1 // Coherent Path to Memory
#define SMMUV3_STREAM_TABLE_ENTRY_DACS 1 // Device attributes are Cacheable and Inner-Shareable
#define SMMUV3_STREAM_TABLE_ENTRY_CONFIG_STAGE_2_TRANSLATE_STAGE_1_BYPASS 0x6 // Stage 2 Translate, Stage 1 Bypass
#define SMMUV3_STREAM_TABLE_ENTRY_CONFIG_STAGE_1_TRANSLATE_STAGE_2_BYPASS 0x5 // Stage 1 Translate, Stage 2 Bypass
#define SMMUV3_STREAM_TABLE_ENTRY_CONFIG_STAGE_2_BYPASS_STAGE_1_BYPASS 0x4 // Stage 2 Bypass, Stage 1 Bypass
#define SMMUV3_STREAM_TABLE_ENTRY_EATS_NOT_SUPPORTED 0 // ATS not supported
#define SMMUV3_STREAM_TABLE_ENTRY_S2VMID 1 // Pick non-zero value
#define SMMUV3_STREAM_TABLE_ENTRY_S2TG_4KB 0 // Granule size 4KB
#define SMMUV3_STREAM_TABLE_ENTRY_S2AA64 1 // AA64
#define SMMUV3_STREAM_TABLE_ENTRY_S2TTB_OFFSET 4 // S2TTB offset >> 4
#define SMMUV3_STREAM_TABLE_ENTRY_S2PTW 1 // S2PTW bit
#define SMMUV3_STREAM_TABLE_ENTRY_S2SL0 2 // Encoded level of page table walk, 2 -> 4-level page table starting from 0
#define SMMUV3_STREAM_TABLE_ENTRY_OUTPUT_ADDRESS_MAX 48 // 48 bit output address width max
#define SMMUV3_STREAM_TABLE_ENTRY_S2RS_RECORD_FAULTS 2 // Record faults
#define SMMUV3_STREAM_TABLE_ENTRY_SHCFG_INCOMING_SHAREABILITY 1 // Incoming shareability attribute
#define SMMUV3_STREAM_TABLE_ENTRY_SHCFG_INNER_SHAREABLE 3 // Inner shareable
#define SMMUV3_STREAM_TABLE_ENTRY_MTCFG 1 // MTCFG bit
#define SMMUV3_STREAM_TABLE_ENTRY_MEMATTR_INNER_OUTTER_WRITEBACK_CACHED 0xF // Inner+Outer write-back cached
#define SMMUV3_STREAM_TABLE_ENTRY_VALID 1 // Entry is valid
//
// SMMUV3 Stage 1 Stream Table Entry bit definitions (used when
// Config = STAGE_1_TRANSLATE_STAGE_2_BYPASS). The S2VMID field is
// ignored when only Stage 1 is enabled and TLB entries
// are tagged with VMID = 0, so STE.S2Vmid is left 0 and every Stage 1
// TLB invalidation targets VMID = 0.
//
#define SMMUV3_STREAM_TABLE_ENTRY_S1CONTEXTPTR_OFFSET 6 // S1ContextPtr address is stored shifted right by 6
#define SMMUV3_STREAM_TABLE_ENTRY_S1FMT_LINEAR 0 // Linear single-CD format
#define SMMUV3_STREAM_TABLE_ENTRY_S1CDMAX_SINGLE_CD 0 // 2^0 = 1 CD, no SubStreamID support
#define SMMUV3_STREAM_TABLE_ENTRY_S1DSS_ABORT 0x2 // Default SubStreamID handling: abort untagged DMA
#define SMMUV3_STREAM_TABLE_ENTRY_S1STALLD_TERMINATE 1 // Force Stage 1 stalling faults to terminate
#define SMMUV3_STREAM_TABLE_ENTRY_S1_ONLY_VMID 0 // Per §5.2: S2Vmid ignored, TLB tagged VMID 0
//
// SMMUV3 Context Descriptor field values used by the Stage 1 CD.
//
#define SMMUV3_CD_TG0_4KB 0 // 4KB granule
#define SMMUV3_CD_AA64 1 // AArch64 translation regime
#define SMMUV3_CD_TTB0_OFFSET 4 // Ttb0 is stored shifted right by 4
#define SMMUV3_CD_EPD1 1 // Disable TTBR1 (TTBR0-only walk)
//
// CD.Ars packed { A(bit2), R(bit1), S(bit0) }: 0x6 = abort + record fault
// event (visible in event queue).
//
#define SMMUV3_CD_ARS_ABORT_RECORD 0x6
#define SMMUV3_CD_MAIR_ATTR0_NORMAL_WBWA 0xFFULL // MAIR[0] = Normal Inner+Outer WBWA
#define SMMUV3_CD_MAIR_ATTR1_DEVICE_NGNRNE 0x00ULL // MAIR[1] = Device-nGnRnE
#define SMMUV3_CD_MAIR0_NORMAL_WBWA (SMMUV3_CD_MAIR_ATTR0_NORMAL_WBWA | (SMMUV3_CD_MAIR_ATTR1_DEVICE_NGNRNE << 8))
// VMID 0 is not a valid VMID so whenever we wrap past the max VMID, we set it to 0 to mark exhaustion.
// (Applies to both 8/16-bit VMID width).
#define SMMU_VMID_RESERVED 0
// ASID 0 is treated the same way as VMID but for the Stage 1 ASID allocator.
#define SMMU_ASID_RESERVED 0
//
// SMMUV3 Configuration bit definitions
//
#define SMMUV3_STR_TAB_BASE_CFG_FMT_LINEAR 0 // Linear Stream Table format
#define SMMUV3_STR_TAB_BASE_CFG_FMT_2LEVEL 1 // 2-Level Stream Table format
#define SMMUV3_STR_TAB_BASE_CFG_SPLIT 6 // Split bit for 2-Level Stream Table
#define SMMUV3_STR_TAB_BASE_L2_PTR_OFFSET 6 // Offset of L2 pointer in the L1 stream table entry
#define SMMUV3_STR_TAB_BASE_ADDR_OFFSET 6 // Stream Table base address offset
#define SMMUV3_STR_TAB_BASE_CMDQ_OFFSET 5 // Command queue base address offset
#define SMMUV3_STR_TAB_BASE_EVENTQ_OFFSET 5 // Event queue base address offset
#define SMMUV3_CR2_E2H 0 // E2H bit 0
#define SMMUV3_CR2_REC_INV_SID 1 // Record C_BAD_STREAMID for invalid input streams
#define SMMUV3_CR2_PTM 1 // PTM bit
#define SMMUV3_CR0_EVENTQ_EN 1 // Event queue enable
#define SMMUV3_CR0_CMDQ_EN 1 // Command queue enable
#define SMMUV3_CR0_SMMU_EN 1 // SMMU enable
#define SMMUV3_CR0_EVENTQ_EN 1 // Event queue enable
#define SMMUV3_CR0_CMDQ_EN 1 // Command queue enable
#define SMMUV3_CR0_PRIQ_EN_DISABLED 0 // Disable PRI queue
#define SMMUV3_CR0_VMW_DISABLED 0 // Disable VMID wildcard matching
#define SMMUV3_CR0_ATS_CHK_DISABLE 1 // Disable bypass for ATS translated traffic
typedef enum _SMMU_ADDRESS_SIZE_TYPE {
SmmuAddressSize32Bit = 0,
SmmuAddressSize36Bit = 1,
SmmuAddressSize40Bit = 2,
SmmuAddressSize42Bit = 3,
SmmuAddressSize44Bit = 4,
SmmuAddressSize48Bit = 5,
SmmuAddressSize52Bit = 6,
} SMMU_ADDRESS_SIZE_TYPE;
typedef struct _RMR_NODE_INFO {
EFI_ACPI_6_0_IO_REMAPPING_RMR_NODE *RmrNode; // Pointer to the RMR node
LIST_ENTRY Link; // Link to the RMR node in the list
} RMR_NODE_INFO;
// Single node in a StreamID list returned by DeviceHandleToStreamId.
// Allocated from pool; freed by SmmuStreamIdListFree.
typedef struct {
LIST_ENTRY Link;
UINT32 StreamId;
} SMMU_STREAM_ID_ENTRY;
// General SMMU Information for a SMMU instance
typedef struct _SMMU_INFO {
VOID *SharedAbortL2; // 2-level only: shared L2 page of all-ABORT STEs. L1 entries point here until split-on-write.
VOID *StreamTable;
VOID *CommandQueue;
VOID *EventQueue;
LIST_ENTRY RmrNodeList;
UINT64 SmmuBase;
UINT64 CachedProducer;
UINT64 CachedConsumer;
UINT32 StreamTableSize;
UINT32 StreamTableEntryMax;
UINT32 Flags;
UINT32 CommandQueueSize;
UINT32 EventQueueSize;
UINT32 StreamTableLog2Size;
UINT32 CommandQueueLog2Size;
UINT32 EventQueueLog2Size;
UINT32 OutputAddressWidth;
UINT8 TranslationStartingLevel;
BOOLEAN PageTableRootConcatenated;
BOOLEAN RangeInvalidationSupported;
BOOLEAN EBSBehaviorAbort;
BOOLEAN Enabled;
BOOLEAN TwoLevelStreamTableSupported; // Whether the SMMU supports 2-level stream tables, which allows more entries than can fit in a single page.
BOOLEAN Vmid16Supported; // IDR0.VMID16. FALSE = 8-bit VMIDs only.
UINT16 NextVmid; // Next per-stream VMID to hand out. Starts at 1.
//
// Per-SMMU translation stage. Defaults to SmmuTranslationStage2.
//
SMMU_TRANSLATION_STAGE TranslationStage;
//
// Stage 1-only fields populated during SmmuV3Configure when
// TranslationStage == SmmuTranslationStage1.
//
BOOLEAN Asid16Supported; // IDR0.ASID16. FALSE = 8-bit ASIDs.
UINT16 NextAsid; // Next per-stream ASID to hand out. Starts at 1.
UINTN EvtqIrqNum;
UINTN GerrIrqNum;
} SMMU_INFO;
// IoMmu configuration structure
typedef struct _IOMMU_CONFIG {
UINT32 SmmuCount;
SMMU_INFO *SmmuInfo;
// Platform-provided UniqueId -> Named Component ObjectName mapping for NonDiscoverable devices.
SMMU_NC_DEVICE_ENTRY *NcDeviceList;
UINT32 NcDeviceCount;
} IOMMU_CONFIG;
// IOMMU/SMMU instance
extern IOMMU_CONFIG *mIoMmu;
extern EFI_HARDWARE_INTERRUPT2_PROTOCOL *mGicInterrupt;
extern VOID *mIortData; // Global IORT data pointer
/**
Decode the address width from the given address size type.
@param [in] AddressSizeType The address size type.
@return The decoded address width. 0 if the address size type is invalid.
**/
UINT32
SmmuV3DecodeAddressWidth (
IN UINT32 AddressSizeType
);
/**
Encode the address width to the corresponding address size type.
@param [in] AddressWidth The address width.
@return The encoded address size type. 0 if the address width is invalid.
**/
UINT8
SmmuV3EncodeAddressWidth (
IN UINT32 AddressWidth
);
/**
Set the translation starting level for SMMUv3 page tables.
Only 3 and 4 level paging are supported.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [in] OutputAddressWidth The output address width.
@param [out] S2Sl0 The starting level for stage 2 translation.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid parameter.
**/
EFI_STATUS
SmmuV3SetTranslationStartingLevel (
IN SMMU_INFO *SmmuInfo,
IN UINT32 OutputAddressWidth,
OUT UINT64 *S2Sl0
);
/**
Read a 32-bit value from the specified SMMU register.
@param [in] SmmuBase The base address of the SMMU.
@param [in] Register The offset of the register.
@return The 32-bit value read from the register. 0 if the SMMU base address is invalid.
**/
UINT32
SmmuV3ReadRegister32 (
IN UINT64 SmmuBase,
IN UINT64 Register
);
/**
Read a 64-bit value from the specified SMMU register.
@param [in] SmmuBase The base address of the SMMU.
@param [in] Register The offset of the register.
@return The 64-bit value read from the register. 0 if the SMMU base address is invalid.
**/
UINT64
SmmuV3ReadRegister64 (
IN UINT64 SmmuBase,
IN UINT64 Register
);
/**
Write a 32-bit value to the specified SMMU register.
@param [in] SmmuBase The base address of the SMMU.
@param [in] Register The offset of the register.
@param [in] Value The 32-bit value to write.
@return The 32-bit value written to the register, or 0 if the SMMU base address is invalid.
**/
UINT32
SmmuV3WriteRegister32 (
IN UINT64 SmmuBase,
IN UINT64 Register,
IN UINT32 Value
);
/**
Write a 64-bit value to the specified SMMU register.
@param [in] SmmuBase The base address of the SMMU.
@param [in] Register The offset of the register.
@param [in] Value The 64-bit value to write.
@return The 64-bit value written to the register, or 0 if the SMMU base address is invalid.
**/
UINT64
SmmuV3WriteRegister64 (
IN UINT64 SmmuBase,
IN UINT64 Register,
IN UINT64 Value
);
/**
Disable interrupts for the SMMUv3.
@param [in] SmmuBase The base address of the SMMU.
@param [in] ClearStaleErrors Whether to clear stale errors.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
@retval EFI_TIMEOUT Timeout.
**/
EFI_STATUS
SmmuV3DisableInterrupts (
IN UINT64 SmmuBase,
IN BOOLEAN ClearStaleErrors
);
/**
Enable interrupts for the SMMUv3.
@param [in] SmmuBase The base address of the SMMU.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
@retval EFI_TIMEOUT Timeout.
**/
EFI_STATUS
SmmuV3EnableInterrupts (
IN UINT64 SmmuBase
);
/**
Disable translation for the SMMUv3.
@param [in] SmmuBase The base address of the SMMU.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
@retval EFI_TIMEOUT Timeout.
**/
EFI_STATUS
SmmuV3DisableTranslation (
IN UINT64 SmmuBase
);
/**
Set the Smmu in ABORT mode and stop DMA.
@param [in] SmmuReg Base address of the SMMUv3.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
@retval EFI_TIMEOUT Timeout.
**/
EFI_STATUS
SmmuV3GlobalAbort (
IN UINT64 SmmuBase
);
/**
Set all streams to bypass the SMMU.
@param [in] SmmuReg Base address of the SMMUv3.
@retval EFI_SUCCESS Success.
@retval EFI_TIMEOUT Timeout.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
**/
EFI_STATUS
SmmuV3SetGlobalBypass (
IN UINT64 SmmuBase
);
/**
Poll the SMMU register and test the value based on the mask.
@param [in] SmmuBase Base address of the SMMU.
@param [in] SmmuReg The SMMU register to poll.
@param [in] Mask Mask of register bits to monitor.
@param [in] Value Expected value.
@retval EFI_SUCCESS Success.
@retval EFI_TIMEOUT Timeout.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
**/
EFI_STATUS
SmmuV3Poll (
IN UINT64 SmmuBase,
IN UINT64 SmmuReg,
IN UINT32 Mask,
IN UINT32 Value
);
/**
Consume the event queue for errors and retrieve the fault record.
Clears the outputted FaultRecord if the queue is empty.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [out] FaultRecord Pointer to the fault record structure.
@param [out] IsEmpty Flag to indicate if the queue is empty.
@retval EFI_SUCCESS Success.
@retval EFI_TIMEOUT Timeout.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
**/
EFI_STATUS
SmmuV3ConsumeEventQueueForErrors (
IN SMMU_INFO *SmmuInfo,
OUT SMMUV3_FAULT_RECORD *FaultRecord,
OUT BOOLEAN *IsEmpty
);
/**
Dump the page table entries for a given virtual address.
Dumps PTE's for all levels regardless of the starting level chosen for translation.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [in] VirtualAddress The virtual address to dump.
@param [in] Root Pointer to the root page table.
**/
VOID
SmmuV3DumpPageTableEntries (
IN SMMU_INFO *SmmuInfo,
IN UINT64 VirtualAddress,
IN PAGE_TABLE *Root
);
/**
Check if an entire address range has a valid identity mapping in the
stage-2 translation table.
Walks Root using SmmuInfo's translation parameters for every 4 KB page in
[Address, Address + Pages * EFI_PAGE_SIZE). Address is rounded down to the
enclosing page. Since this driver identity-maps DMA a page counts as mapped only when:
- every intermediate level has a non-zero descriptor,
- the leaf entry has PAGE_TABLE_ENTRY_VALID_BIT set, and
- the leaf entry's encoded physical address matches the page address.
Returns TRUE only if every page in the range satisfies the above; FALSE on
the first page that fails (short-circuit), bad parameters, or Pages == 0.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure whose translation
parameters drive the walk.
@param [in] Root Pointer to the root page table.
@param [in] Address Start address of the range (same value used as both
VA and PA under identity mapping). Aligned down to
the enclosing 4 KB page.
@param [in] Pages Number of 4 KB pages to check, starting at Address.
@retval TRUE Every page in the range has a valid identity-mapped leaf.
@retval FALSE At least one page is not mapped, encodes a different PA, or
a required parameter is invalid.
**/
BOOLEAN
SmmuV3IsAddressRangeMapped (
IN SMMU_INFO *SmmuInfo,
IN PAGE_TABLE *Root,
IN UINT64 Address,
IN UINTN Pages
);
/**
Log the errors if found from the SMMUv3. Prints Event Queue entries and GError register.
Does nothing if no errors found.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@retval EFI_SUCCESS No SMMU errors found.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
@retval EFI_DEVICE_ERROR SMMU error found.
**/
EFI_STATUS
SmmuV3LogErrors (
IN SMMU_INFO *SmmuInfo
);
/**
Register GIC interrupt sources for SmmuV3 EVTQ and GERR interrupts.
@param[in] GicInterrupt Pointer to the GIC interrupt protocol.
@param[in] SmmuInfo Pointer to the SMMU_INFO structure.
@retval EFI_SUCCESS The interrupt sources were registered successfully.
@retval EFI_INVALID_PARAMETER The GicInterrupt or SmmuInfo is NULL.
**/
EFI_STATUS
SmmuV3RegisterGicIsr (
IN EFI_HARDWARE_INTERRUPT2_PROTOCOL *GicInterrupt,
IN SMMU_INFO *SmmuInfo
);
/**
Send a SMMUV3_CMD_GENERIC command to the SMMUv3.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [in] Command Pointer to the command to send.
@retval EFI_SUCCESS Success.
@retval EFI_TIMEOUT Timeout.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
**/
EFI_STATUS
SmmuV3SendCommand (
IN SMMU_INFO *SmmuInfo,
IN SMMUV3_CMD_GENERIC *Command
);
/**
Invalidate all Stage 2 TLB entries owned by the given VMID on this SMMU.
Uses CMD_TLBI_S12_VMALL.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [in] Vmid The VMID to invalidate.
@retval EFI_SUCCESS Success.
@retval EFI_TIMEOUT Timeout.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
**/
EFI_STATUS
SmmuV3TLBInvalidateAllStage2 (
IN SMMU_INFO *SmmuInfo,
IN UINT16 Vmid
);
/**
Invalidate all Stage 1 TLB entries owned by the given ASID on this SMMU.
Uses CMD_TLBI_NH_ASID with VMID = 0 (Stage 1 only mode tags TLBs with VMID = 0).
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [in] Asid ASID to invalidate.
@retval EFI_SUCCESS Success.
@retval EFI_TIMEOUT Timeout.
@retval EFI_INVALID_PARAMETER Invalid Parameters.
**/
EFI_STATUS
SmmuV3TLBInvalidateAllStage1 (
IN SMMU_INFO *SmmuInfo,
IN UINT16 Asid
);
/**
* Add RMR mappings for each SMMU node in the SmmuInfo structure.
* This function iterates through the RMR nodes and updates the page table
* for each memory range described in the RMR node.
*
* @param [in] SmmuInfo Pointer to the SMMU_INFO structure.
*
* @retval EFI_SUCCESS Success.
* @retval EFI_INVALID_PARAMETER Invalid Parameters.
* @retval Other RMR mapping update failure.
*/
EFI_STATUS
SmmuV3AddRMRMapping (
IN SMMU_INFO *SmmuInfo
);
/**
* Parse IORT table and extract SMMU information
*
* @param[in] IortTable Pointer to the IORT table
* @param[out] SmmuInfo Pointer to store the array of SMMU_INFO structures
* @param[out] SmmuCount Pointer to store the number of SMMU nodes found
*
* @return EFI_SUCCESS on success
* @return EFI_INVALID_PARAMETER if any parameter is NULL
* @return EFI_OUT_OF_RESOURCES if memory allocation fails
* @return EFI_NOT_FOUND if no SMMU nodes are found
* @return EFI_UNSUPPORTED if the IORT table is not supported
*/
EFI_STATUS
SmmuV3ParseIort (
IN VOID *IortTable,
OUT SMMU_INFO **SmmuInfo,
OUT UINT32 *SmmuCount
);
/**
Allocate a per-stream page-table root suitable for use as an STE's S2TTB
(Stage 2) or CD.TTB0 (Stage 1). The root size is derived from
SmmuInfo->TranslationStage: Stage 2 allocates
PAGE_TABLE_ROOT_STAGE2_PAGES for concatenation at the starting level;
Stage 1 allocates PAGE_TABLE_ROOT_STAGE1_PAGES since concatenation is
not architecturally allowed.
@param [in] SmmuInfo SMMU instance whose TranslationStage decides how
many pages the root occupies.
@retval Pointer to the zeroed root, or NULL on failure.
**/
PAGE_TABLE *
SmmuV3AllocatePageTableRoot (
IN SMMU_INFO *SmmuInfo
);
/**
Recursively free a per-stream page-table tree previously returned by
SmmuV3AllocatePageTableRoot(). SmmuInfo->TranslationStage is consulted
at Level == 0 to release the correct number of pages for the root.
@param [in] SmmuInfo SMMU instance the tree was allocated for.
@param [in] Level Current level (caller must pass 0 for the root).
@param [in] PageTable The page-table tree to free. May be NULL.
**/
VOID
SmmuV3FreePageTableTree (
IN SMMU_INFO *SmmuInfo,
IN UINT8 Level,
IN PAGE_TABLE *PageTable
);
/**
Ensure a stage-2 page-table root exists for the given StreamID. If this is
the first call for the StreamID on this SMMU, allocate a fresh root + VMID
and promote the corresponding STE from ABORT to STAGE_2_TRANSLATE using a
break-before-make sequence (CFGI_STE + CMD_SYNC twice). Subsequent calls
return the (Root, Vmid) already encoded in the live STE.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [in] StreamId The StreamID.
@param [out] OutRoot Receives the stage-2 page-table root.
@param [out] OutVmid Receives the VMID tag installed in the STE.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid parameters.
@retval EFI_OUT_OF_RESOURCES Allocation failed / VMID space exhausted.
@retval Other STE promotion failure.
**/
EFI_STATUS
SmmuV3StreamGetOrCreate (
IN SMMU_INFO *SmmuInfo,
IN UINT32 StreamId,
OUT PAGE_TABLE **OutRoot,
OUT UINT16 *OutVmid
);
/**
Free every SMMU_STREAM_ID_ENTRY hanging off the given list head and
re-initialize the head as empty. Safe to call on an already-empty list.
@param[in,out] StreamIdList List head previously populated by
DeviceHandleToStreamId.
**/
VOID
SmmuStreamIdListFree (
IN OUT LIST_ENTRY *StreamIdList
);
/**
Resolve a DeviceHandle to its IORT-derived StreamID(s).
For real PCIe devices (Segment != 0xFF):
PciIo->GetLocation() -> RID -> IORT RC node ID mapping -> single StreamID.
The matched mapping's OutputReference identifies the owning SMMUv3 node,
whose base address is returned in *SmmuBase.
For NonDiscoverable devices (Segment == 0xFF):
UniqueId -> platform NC table entry -> IORT Named Component node ->
full StreamID list (every mapping expanded, including ranges).
@param[in] IortTable Pointer to the IORT ACPI table.
@param[in] DeviceHandle The device handle to resolve.
@param[in,out] StreamIdList Caller-supplied, initialized-empty list head.
On success contains one SMMU_STREAM_ID_ENTRY
per resolved StreamID, in IORT order (first
entry is the primary; rest are aliases that
share the primary's stage-2 page table and
VMID). Caller must release via
SmmuStreamIdListFree.
@param[out] SmmuBase Optional. If non-NULL, receives the base
address of the SMMUv3 node that owns these
StreamIDs (0 if unknown).
@retval EFI_SUCCESS StreamIDs resolved.
@retval EFI_INVALID_PARAMETER One or more required parameters are NULL.
@retval EFI_UNSUPPORTED DeviceHandle has no PciIo protocol.
@retval EFI_NOT_FOUND No IORT mapping found.
@retval EFI_OUT_OF_RESOURCES Allocation failure while building the list.
**/
EFI_STATUS
DeviceHandleToStreamId (
IN VOID *IortTable,
IN EFI_HANDLE DeviceHandle,
IN OUT LIST_ENTRY *StreamIdList,
OUT UINT64 *SmmuBase
);
/**
Build a default "abort-equivalent" stream-table entry. Equivalent to a
STAGE_2_TRANSLATE STE with S2Ttb = 0 stage 2 is enabled but no
translations are installed, so DMA misses fault and are recorded. Used at
init to populate every STE slot before any device has been mapped.
@param [in] SmmuInfo SMMU instance (needed for IDR-derived fields).
@param [out] StreamEntry STE buffer to populate.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid parameters.
@retval Other Failure from the underlying translate STE builder.
**/
EFI_STATUS
SmmuV3BuildInvalidStreamTableEntry (
IN SMMU_INFO *SmmuInfo,
OUT SMMUV3_STREAM_TABLE_ENTRY *StreamEntry
);
/**
Build a STAGE_2_TRANSLATE stream-table entry using the given page-table root.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [in] PageTableRoot Page-table root the STE should point at.
@param [in] Vmid VMID tag to install in the STE's S2VMID field.
@param [out] StreamEntry STE buffer to populate.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid parameters.
**/
EFI_STATUS
SmmuV3BuildStage2TranslateStreamTableEntry (
IN SMMU_INFO *SmmuInfo,
IN PAGE_TABLE *PageTableRoot,
IN UINT16 Vmid,
OUT SMMUV3_STREAM_TABLE_ENTRY *StreamEntry
);
/**
Locate the STE slot in the SMMU's stream table for a given StreamID.
Supports both linear and 2-level stream tables.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [in] StreamId The StreamID.
@retval Pointer to the STE slot, or NULL if out-of-range.
**/
SMMUV3_STREAM_TABLE_ENTRY *
SmmuV3GetSteSlot (
IN SMMU_INFO *SmmuInfo,
IN UINT32 StreamId
);
/**
Promote the STE for StreamId from Invalid to Valid with the given
page-table root, using the SMMU break-before-make sequence required by the
SMMUv3 spec for STE Config changes:
1. Write the STE with V=0.
2. DSB + CFGI_STE(StreamId) + CMD_SYNC.
3. Write the full new STE contents (S2Ttb etc., Config=S2_TRANSLATE, V=1).
4. DSB + CFGI_STE(StreamId) + CMD_SYNC.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [in] StreamId The StreamID whose STE is being promoted.
@param [in] Vmid VMID tag to install in the STE's S2VMID field.
@param [in] PageTableRoot Page-table root to install in the STE.
@param [in] NewL2 Caller-provided L2 page used by
SmmuV3SplitL1IfShared() when the covering L1
descriptor still points at the shared-ABORT
L2. Because the caller owns the PageTableRoot and NewL2,
this function does not allocate or free them.
@param [out] NewL2Consumed Set to TRUE if NewL2 was installed into an L1
descriptor by the split step (the caller must
NOT free it), FALSE otherwise (the caller
should free NewL2).
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid parameters.
@retval Other Command-queue / sync failure.
**/
EFI_STATUS
SmmuV3PromoteSteToStage2Translate (
IN SMMU_INFO *SmmuInfo,
IN UINT32 StreamId,
IN UINT16 Vmid,
IN PAGE_TABLE *PageTableRoot,
IN SMMUV3_STREAM_TABLE_ENTRY *NewL2,
OUT BOOLEAN *NewL2Consumed
);
/**
Allocate a Context Descriptor (CD) suitable for use as a Stage 1 STE's
S1ContextPtr. The CD is 64-byte aligned as required by the SMMUv3 spec
and zero-initialized (V=0). The caller populates Ttb0 / Asid / TCR
fields via SmmuV3BuildStage1ContextDescriptor before publishing it via
SmmuV3PromoteSteToStage1Translate.
@retval Pointer to the zeroed CD, or NULL on failure.
**/
SMMUV3_CONTEXT_DESCRIPTOR *
SmmuV3AllocateContextDescriptor (
VOID
);
/**
Free a Context Descriptor previously returned by
SmmuV3AllocateContextDescriptor.
@param [in] Cd Context Descriptor to free. May be NULL.
**/
VOID
SmmuV3FreeContextDescriptor (
IN SMMUV3_CONTEXT_DESCRIPTOR *Cd
);
/**
Populate a Context Descriptor for Stage 1 identity-mapped translation.
Sets Ttb0 to the given page-table root, ASID to the supplied per-stream
tag, T0Sz / TG0 / IPS / IR0 / OR0 / SH0 based on the SMMU's output
address width and the platform's coherency configuration, MAIR to
attribute index 0 = Normal WB Inner+Outer, disables TTBR1, and sets
AArch64 + Valid = 1.
@param [in] SmmuInfo SMMU instance (needed for IDR-derived fields).
@param [in] PageTableRoot Stage 1 page-table root to install in Ttb0.
NULL builds an invalid (V = 0) CD suitable
for the init-time template.
@param [in] Asid ASID tag installed in CD.Asid.
@param [out] Cd CD buffer to populate.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid parameters.
@retval Other Failure from starting-level computation.
**/
EFI_STATUS
SmmuV3BuildStage1ContextDescriptor (
IN SMMU_INFO *SmmuInfo,
IN PAGE_TABLE *PageTableRoot,
IN UINT16 Asid,
OUT SMMUV3_CONTEXT_DESCRIPTOR *Cd
);
/**
Build a STAGE_1_TRANSLATE / STAGE_2_BYPASS stream-table entry that
points at the supplied Context Descriptor. Passing Cd = NULL builds an
invalid (Valid = 0) STE suitable for the init-time template.
@param [in] SmmuInfo SMMU instance.
@param [in] Cd CD the STE's S1ContextPtr should point at, or
NULL to build an invalid STE.
@param [out] StreamEntry STE buffer to populate.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid parameters.
**/
EFI_STATUS
SmmuV3BuildStage1TranslateStreamTableEntry (
IN SMMU_INFO *SmmuInfo,
IN SMMUV3_CONTEXT_DESCRIPTOR *Cd,
OUT SMMUV3_STREAM_TABLE_ENTRY *StreamEntry
);
/**
Promote the STE for StreamId from ABORT to STAGE_1_TRANSLATE /
STAGE_2_BYPASS with the supplied CD, using the SMMU break-before-make
sequence required for STE Config changes.
@param [in] SmmuInfo Pointer to the SMMU_INFO structure.
@param [in] StreamId The StreamID whose STE is being promoted.
@param [in] Cd Context Descriptor to install in the STE.
@param [in] NewL2 Caller-provided L2 page used by
SmmuV3SplitL1IfShared() when the covering L1
descriptor still points at the shared-ABORT
L2. Because the caller owns Cd and NewL2,
this function does not allocate or free them.
@param [out] NewL2Consumed Set to TRUE if NewL2 was installed into an L1
descriptor by the split step (the caller must
NOT free it), FALSE otherwise (the caller
should free NewL2).
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid parameters.
@retval Other Command-queue / sync failure.
**/
EFI_STATUS
SmmuV3PromoteSteToStage1Translate (
IN SMMU_INFO *SmmuInfo,
IN UINT32 StreamId,
IN SMMUV3_CONTEXT_DESCRIPTOR *Cd,
IN SMMUV3_STREAM_TABLE_ENTRY *NewL2,
OUT BOOLEAN *NewL2Consumed
);
/**
Update the page table mapping with the given physical address and attributes.
@param [in] SmmuInfo SMMU instance.
@param [in] Root Pointer to the root page table.
@param [in] TagId Per-stream tag whose TLB entries
should be invalidated on unmap.
VMID for Stage 2 SMMUs, ASID for
Stage 1 SMMUs.
@param [in] PhysicalAddress Physical address to map.
@param [in] Bytes Number of bytes to map.
@param [in] Attributes Attributes to set for the mapping. Must be a valid Stage 2 Translation Table attribute (12 bits or less).
@param [in] Valid Boolean to indicate if the entry is valid.
@retval EFI_SUCCESS Success.
@retval EFI_INVALID_PARAMETER Invalid parameter.
@retval EFI_OUT_OF_RESOURCES Out of resources.
**/
EFI_STATUS
UpdatePageTable (
IN SMMU_INFO *SmmuInfo,
IN PAGE_TABLE *Root,
IN UINT16 TagId,
IN UINT64 PhysicalAddress,
IN UINT64 Bytes,
IN UINT16 Attributes,
IN BOOLEAN Valid
);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
/** @file
File for SMMU config structures.
This SMMU_CONFIG structure is used to pass the SMMU configuration data from
the platform to the SMMU driver. The Smmu driver will use this data to install
the IORT table and configure the SMMU hardware.
Given the IORT is configurable and platform dependent, the SMMU_CONFIG structure contains
all info relevant to the IORT table and SMMUv3 platform specific configuration.
See <https://developer.arm.com/documentation/den0049/latest/> for IORT spec.
Copyright (c) Microsoft Corporation.
SPDX-License-Identifier: BSD-2-Clause-Patent
**/
#pragma once
// Increment SMMU_CONFIG version when the structure changes.
// Backwards compatibility is currently not supported.
// Future backwards compatibility is only possible if new fields are added to the end of the structure and existing fields are not modified.
// SmmuDxe driver will check and enforce the version of the SMMU_CONFIG structure to this current version set here.
#define CURRENT_SMMU_CONFIG_VERSION_MAJOR 1
#define CURRENT_SMMU_CONFIG_VERSION_MINOR 1
#pragma pack(push, 1)
// Maximum length (including NULL) of the ACPI namespace path stored for a
// NonDiscoverable device entry. Must be large enough to hold the
// IORT Named Component ObjectName
#define SMMU_NC_DEVICE_OBJNAME_MAX 32
// Per-SMMU translation stage selection. Every SMMU described by the HOB is
// configured for the same stage.
typedef enum _SMMU_TRANSLATION_STAGE {
SmmuTranslationStage2 = 0, // Stage 2 translate, Stage 1 bypass
SmmuTranslationStage1 = 1, // Stage 1 translate, Stage 2 bypass
} SMMU_TRANSLATION_STAGE;
// Platform-provided lookup entry mapping a NonDiscoverable device's
// PciIo->GetLocation()-derived UniqueId to the SMMU StreamID's associated
// with the NamedComponent node in the IORT.
// Each NonDiscoverable device exposes a UniqueId from the NonDiscoverableDeviceRegistrationLib.
// This is used to determine a determinstic PciIo->GetLocation().
typedef struct _SMMU_NC_DEVICE_ENTRY {
UINT64 UniqueId; // Value from the NON_DISCOVERABLE_DEVICE_UNIQUE_ID protocol on the handle.
CHAR8 ObjName[SMMU_NC_DEVICE_OBJNAME_MAX]; // IORT Named Component ObjectName (NUL-terminated). SmmuDxe walks the IORT for the matching NC node to recover the owning SMMU base and the full StreamID list.
} SMMU_NC_DEVICE_ENTRY;
// SMMU_CONFIG structure to pass the SMMU configuration data from the platform to the SMMU driver.
// Platform will configure SmmuDisabledList size and offset to the SMMU disabled list appropriatley
// for any SMMU that needs be disabled in UEFI and set to bypass.
typedef struct _SMMU_CONFIG {
UINT32 VersionMajor;
UINT32 VersionMinor;
UINT32 SmmuDisabledListSize; // Size of SmmuDisabledList in bytes.
UINT32 SmmuDisabledListOffset; // Offset in bytes to the SmmuDisabledList from the start of the HOB structure.
UINT32 IortSize;
UINT32 IortOffset; // Offset in bytes to the IORT table from the start of the HOB structure.
UINT32 NcDeviceListSize; // Size of the NonDiscoverable device lookup array in bytes (multiple of sizeof(SMMU_NC_DEVICE_ENTRY)).
UINT32 NcDeviceListOffset; // Offset in bytes to the NonDiscoverable device lookup array from the start of the HOB structure. 0 if absent.
SMMU_TRANSLATION_STAGE TranslationStage; // SmmuTranslationStage2 (default) or SmmuTranslationStage1.
// SmmuDxe will configure every SMMU described by this HOB to the same stage.
// Any value other than SmmuTranslationStage1 is treated as SmmuTranslationStage2.
} SMMU_CONFIG;
#pragma pack(pop)
#define SMMU_CONFIG_HOB_GUID \
{ 0xcd56ec8f, 0x75f1, 0x440a, { 0xaa, 0x48, 0x09, 0x58, 0xb1, 0x1c, 0x9a, 0xa7 } }
extern EFI_GUID gSmmuConfigHobGuid;

File diff suppressed because it is too large Load diff