IOMMU
더 많은 작업
- Input-Output Memory Management Unit, IOMMU; 입출력 메모리 관리 장치
- DMA를 하는 입출력 장치와 주기억장치 사이에서 장치가 내는 주소를 바꾸고 접근을 검사하는 메모리 관리 장치
IOMMU는 장치 쪽의 메모리 관리 장치다. CPU의 MMU가 프로그램이 쓰는 가상 주소를 물리 주소로 바꾸듯, IOMMU는 GPU, 네트워크 카드, NVMe 저장 장치 같은 장치가 DMA(직접 메모리 접근)로 내는 주소(장치 주소, IOVA)를 물리 주소로 바꾼다. 운영체제가 장치마다 허락한 페이지만 표에 올려 두므로, 장치가 그 밖의 메모리를 읽거나 쓰려 하면 IOMMU가 막고 오류를 보고한다.
DMA는 CPU를 거치지 않으므로 CPU의 MMU와 메모리 보호를 완전히 우회한다. IOMMU가 없으면 장치나 드라이버 버그 하나가 커널 메모리를 덮어쓰고, 악의적인 장치는 전체 메모리를 읽을 수 있다. IOMMU는 이 구멍을 막는 한편, 가상 머신에 물리 장치를 직접 넘겨주는 PCI 패스스루를 가능하게 한다.
- DMA 재매핑(DMA remapping): 장치별 입출력 페이지 테이블로 장치 주소를 물리 주소로 바꾼다. 흩어진 물리 페이지를 장치에는 연속된 주소로 보여 줄 수 있어, 장치가 분산·수집(scatter-gather)을 지원하지 않아도 큰 버퍼를 한 번에 전송한다.
- 장치 격리: 장치마다(정확히는 PCI 요청자 ID마다) 다른 표를 두어, 장치가 자기에게 할당된 버퍼 말고는 접근하지 못하게 한다. 메모리 보호는 CPU에서 도는 운영체제가 MMU와 IOMMU를 모두 독점적으로 설정한다는 데 기댄다. 장치는 이 표를 스스로 바꾸지 못한다.
- 주소 폭 보완: 32비트 주소만 내는 오래된 PCI 장치는 4GB 위 메모리에 직접 접근하지 못한다. IOMMU가 없으면 운영체제가 4GB 아래에 바운스 버퍼를 두고 매번 복사해야 하지만(리눅스의 SWIOTLB), IOMMU가 있으면 4GB 아래의 장치 주소를 높은 물리 주소로 매핑하면 된다.
- 인터럽트 재매핑(interrupt remapping): 장치가 메모리 쓰기 형태로 보내는 MSI/MSI-X 인터럽트도 표로 검사하고 대상 CPU와 벡터를 바꾼다. 가상 머신에 넘긴 장치가 가짜 인터럽트로 호스트를 공격하는 것을 막고, 게스트에 인터럽트를 바로 전달하는 기능(인텔 posted interrupt, AMD AVIC)의 바탕이 된다.
- 장치 쪽 페이지 요청: PCIe의 주소 변환 서비스(ATS)를 지원하는 장치는 변환 결과를 자기 안의 장치 TLB에 캐시하고, 페이지 요청 인터페이스(PRI)로 없는 페이지를 운영체제에 요청할 수 있다. 공유 가상 주소(SVA)로 장치와 프로세스가 같은 가상 주소를 쓰는 데 쓴다.
| 항목 | MMU | IOMMU |
|---|---|---|
| 주소를 내는 쪽 | CPU 코어 | DMA를 하는 장치 |
| 표를 고르는 기준 | 현재 프로세스(CR3, TTBR) | 장치의 요청자 ID(PCI 버스:장치.기능), PASID |
| 변환 캐시 | TLB | IOTLB, 장치 쪽 ATC |
| 위반 시 | 페이지 폴트 예외, 운영체제가 처리 후 재실행 | 대개 DMA 거부와 오류 기록. 재시도는 PRI 지원 장치만 |
| 위치 | CPU 코어 안 | 예전엔 칩셋(노스브리지), 지금은 CPU 안의 PCIe 루트 복합체 옆 |
| 이름 | 업체 | 특징 |
|---|---|---|
| GART (Graphics Address Remapping Table) | AGP, 초기 PCIe 그래픽 | 그래픽 카드용 조리개(aperture) 영역만 재매핑하는 원시적 IOMMU. 보호 기능은 거의 없다. AMD K8은 이를 리눅스에서 일반 장치용 IOMMU로도 썼다 |
| VT-d (Virtualization Technology for Directed I/O) | 인텔 | DMA 재매핑, 인터럽트 재매핑, 장치별 컨텍스트 표. ACPI DMAR 표로 보고. 스케일러블 모드는 PASID 단위 변환 지원 |
| AMD-Vi (AMD I/O Virtualization Technology) | AMD | 장치 표, 입출력 페이지 테이블, 인터럽트 재매핑. ACPI IVRS 표로 보고 |
| SMMU (System MMU) | Arm | SMMUv1~v3. 스트림 ID로 장치를 구분하고 CPU와 같은 형식의 Stage 1(프로세스)과 Stage 2(가상 머신) 변환을 한다 |
| DART | 애플 | 애플 실리콘 등에서 장치별 DMA 주소 변환 |
| TCE (Translation Control Entry) | IBM 파워 | 논리 분할(LPAR) 사이 장치 격리 |
| DVMA | 선 스팍 | 장치용 가상 메모리 접근 |
IBM은 1979년 4300 계열의 ECPS:VSE 모드에서 채널 프로그램이 가상 주소를 쓰게 했다. PCI-SIG는 SR-IOV와 ATS를 정했고, PCI Express 5.0부터 기본 규격에 합쳤다.
인텔은 2006년 VT-d를 소개했고,[1] AMD는 AMD-Vi 규격을 공개하고 있다.[2][3] 노스브리지와 사우스브리지로 나뉘던 시절에는 칩셋이 IOMMU를 맡았고, 메모리 컨트롤러와 PCIe가 CPU로 들어오면서 IOMMU도 CPU 쪽으로 옮겨 왔다. 메인보드 설정 화면에서는 인텔은 VT-d, AMD는 IOMMU 또는 AMD-Vi라는 이름으로 켜고 끈다. CPU 가상화 기능(인텔 VT-x, AMD SVM)과는 별개 설정이다.
게스트 운영체제는 자기가 보는 게스트 물리 주소가 실제 호스트 물리 주소와 다르다는 것을 모른다. 게스트 드라이버가 장치에 게스트 물리 주소로 DMA를 지시하면, IOMMU가 없는 한 장치는 엉뚱한 호스트 메모리를 쓴다. 하이퍼바이저가 모든 입출력을 가로채 주소를 바꿔 주는 에뮬레이션은 안전하지만 느리다.
IOMMU가 있으면 하이퍼바이저가 그 장치의 입출력 페이지 테이블을 "게스트 물리 주소에서 호스트 물리 주소로" 가는 표로 채워 둔다. 게스트의 기존 드라이버가 그대로 장치를 다루고, DMA는 하이퍼바이저를 거치지 않고 게스트 메모리로만 간다. 이것이 PCI 패스스루(장치 직접 할당)이며, GPU나 고속 NIC를 가상 머신에 넘길 때 쓴다. 하나의 물리 장치를 여러 가상 함수(VF)로 나누는 SR-IOV도 VF마다 IOMMU 격리가 있어야 가상 머신 사이 보호가 성립한다.
리눅스의 VFIO(Virtual Function I/O)는 IOMMU로 보호된 환경에서 장치를 사용자 공간에 직접 넘기는 프레임워크다. QEMU/KVM의 장치 할당과 DPDK, SPDK 같은 사용자 공간 드라이버가 쓴다.[4]
IOMMU 그룹은 시스템의 나머지 장치들과 격리할 수 있는 가장 작은 장치 집합이다. 장치 사이에 IOMMU를 거치지 않는 직접 통신(PCIe 피어 투 피어)이 가능하면 둘을 따로 격리할 수 없으므로 같은 그룹이 된다. 다기능 장치, 접근 제어 서비스(ACS)를 지원하지 않는 PCIe 스위치나 브리지, PCIe에서 PCI로 가는 브리지 아래의 장치들이 한 그룹으로 묶이는 흔한 원인이다. VFIO는 그룹 단위로만 장치를 넘기며, 그룹의 모든 장치가 VFIO나 무해한 드라이버에 묶여 있어야 한다.[4] 소비자용 메인보드에서 GPU가 다른 장치와 한 그룹이 되어 패스스루가 안 되는 일이 흔하고, 이를 억지로 쪼개는 ACS 우회 패치는 격리를 깨므로 보안상 권장되지 않는다.
# IOMMU가 켜졌는지 확인
dmesg | grep -i -e DMAR -e IOMMU -e AMD-Vi
# IOMMU 그룹별 장치 목록
for g in /sys/kernel/iommu_groups/*; do
echo "그룹 ${g##*/}:"; for d in $g/devices/*; do lspci -nns ${d##*/}; done
done
# 장치 0000:06:0d.0 을 vfio-pci 드라이버에 묶기 (커널 문서 예)
modprobe vfio-pci
readlink /sys/bus/pci/devices/0000:06:0d.0/iommu_group # 예: ../../../kernel/iommu_groups/26
echo 0000:06:0d.0 > /sys/bus/pci/devices/0000:06:0d.0/driver/unbind
echo 1102 0002 > /sys/bus/pci/drivers/vfio-pci/new_id # 공급업체 ID, 장치 ID
ls /dev/vfio/ # 26 이 생긴다
| 옵션 | 뜻 |
|---|---|
intel_iommu=on / off |
인텔 IOMMU(DMAR) 드라이버를 켜거나 끈다. 현재 커널의 기본 설정(CONFIG_INTEL_IOMMU_DEFAULT_ON)은 켜짐이지만 배포판 빌드에 따라 다르다
|
intel_iommu=sm_on |
VT-d 스케일러블 모드 사용 |
amd_iommu=off |
AMD IOMMU를 초기화하지 않음. force_isolation은 모든 장치를 강제로 격리
|
iommu=pt |
통과(passthrough) 모드 기본값. 호스트 드라이버의 DMA는 변환 없이 1:1로 보내고, VFIO로 넘긴 장치만 변환한다. iommu.passthrough=1과 같다
|
iommu=nopt |
호스트 장치도 모두 변환(보호 최대, 약간의 성능 비용) |
iommu=off |
IOMMU를 쓰지 않음 |
iommu.strict=1 / 0 |
DMA 해제 때 IOTLB를 즉시 무효화(엄격)하거나 모아서 나중에 무효화(지연). 지연 모드는 처리량이 높지만 해제된 버퍼에 장치가 잠시 접근할 수 있다 |
옵션 설명은 커널 문서를 따랐다.[5] 가상화 호스트에서 흔히 intel_iommu=on iommu=pt를 함께 쓰는 이유는, 패스스루할 장치는 격리하면서 호스트가 쓰는 나머지 장치의 변환 비용은 피하려는 것이다. 다만 통과 모드에서는 호스트 장치에 대한 DMA 공격 방어가 사라진다.
DMA 공격은 DMA를 할 수 있는 외부 포트(파이어와이어, 익스프레스카드, 썬더볼트, USB4)에 장치를 꽂아 메모리를 직접 읽거나 고치는 공격이다. 잠긴 노트북에서 디스크 암호화 키를 빼내거나 커널 코드를 고쳐 잠금 화면을 우회하는 데 쓴다. 몇 분이면 되고, 기기를 분해할 필요도 없어 "지나가다 하는(drive-by)" 공격이라고도 한다.
- 커널 DMA 보호(윈도우): 윈도우 10 1803부터 IOMMU로 썬더볼트, USB4, CFexpress 같은 외부 PCIe 핫플러그 장치의 DMA를 막는다. DMA 재매핑을 지원하는 드라이버의 장치는 바로 동작하고, 지원하지 않는 드라이버의 장치는 사용자가 로그인하거나 화면 잠금을 풀 때까지 시작하지 못한다. UEFI 펌웨어 지원이 필요하며 부팅 중 공격은 막지 못한다. 파이어와이어, PCMCIA, 카드버스, 익스프레스카드는 보호 대상이 아니다.[6][7] 윈도우 10 1903부터는 M.2 슬롯 같은 내부 PCIe 포트도 대상에 넣었다.
- 리눅스: 2018년 이후 썬더볼트 시스템은 펌웨어가 IOMMU 기반 DMA 보호를 알리면 커널이 IOMMU를 자동으로 켠다.
/sys/bus/thunderbolt/devices/domainX/iommu_dma_protection이 1이면 적용된 것이다. 이 경우 썬더볼트 보안 수준(none, user, secure, dponly 등)에 따른 장치 승인은 부차적이 된다.[8] - 썬더클랩(Thunderclap): 2019년 NDSS에서 케임브리지 대학교 등의 연구진이 발표한 연구다. FPGA로 만든 가짜 네트워크 카드를 썬더볼트에 꽂아, IOMMU를 쓰는 macOS, FreeBSD, 리눅스에서도 메모리를 읽고 제어를 빼앗을 수 있음을 보였다. IOMMU 자체가 아니라 운영체제가 IOMMU를 쓰는 방식이 문제였다. 네트워크 버퍼와 같은 페이지에 커널 포인터가 든 구조체가 함께 매핑되거나, 해제 후 무효화가 늦거나(지연 모드), 장치를 쉽게 믿어 버리는 드라이버가 공격 경로가 되었다.[9] 이후 리눅스는 외부 포트에 붙은 장치를 신뢰하지 않는 장치로 표시해 엄격한 무효화와 바운스 버퍼를 쓰게 하는 식으로 대응한 것으로 알려져 있다.
- 부팅 중에는 운영체제가 IOMMU를 설정하기 전이므로 펌웨어가 DMA 보호를 맡아야 한다. UEFI 펌웨어의 사전 부팅 DMA 보호(pre-boot DMA protection) 설정이 이 틈을 메운다.
- 변환 비용: 장치의 접근마다 변환이 필요하고, IOTLB 미스면 입출력 페이지 테이블을 걸어야 한다. 특히 매 패킷마다 매핑과 해제를 반복하는 고속 네트워크에서는 매핑 관리와 IOTLB 무효화가 처리량을 떨어뜨린다. IBM 연구진은 2007년 이 비용을 측정해 매핑 재사용 같은 완화책을 제시했다.[10]
- 메모리: 입출력 페이지 테이블이 물리 메모리를 차지한다. CPU와 표를 공유하면 줄어든다.
- 보호 단위: IOMMU는 보통 4KB 페이지 단위로 보호한다. 작은 버퍼 하나를 장치에 보여 주면 같은 페이지의 다른 데이터도 함께 보인다. 민감한 구조체와 장치 버퍼를 같은 페이지에 두지 않으려면 페이지 정렬과 0 채우기, 바운스 버퍼가 필요해 성능이 떨어진다. 썬더클랩이 파고든 지점이 이것이다.
- 격리 단위: IOMMU 그룹보다 잘게 격리할 수 없다. ACS가 없는 하드웨어에서는 여러 장치가 한 덩어리로만 넘어간다.
- 입출력 포트: 메모리와 따로 떨어진 입출력 포트 공간을 쓰는 아키텍처(x86의
in,out)에서는 CPU가 포트로 장치와 통신할 때 IOMMU가 관여하지 않는다.
- ↑ Intel platform hardware support for I/O virtualization, Intel Technology Journal (2006)
- ↑ Intel Virtualization Technology for Directed I/O Architecture Specification, Intel
- ↑ AMD I/O Virtualization Technology (IOMMU) Specification, AMD
- ↑ 4.0 4.1 VFIO: Virtual Function I/O, Linux kernel documentation
- ↑ The kernel's command-line parameters, Linux kernel documentation
- ↑ Kernel DMA Protection, Microsoft Learn
- ↑ Kernel DMA Protection (Memory Access Protection) for OEMs, Microsoft Learn
- ↑ USB4 and Thunderbolt, Linux kernel documentation
- ↑ A. Theodore Markettos 외, Thunderclap: Exploring Vulnerabilities in Operating System IOMMU Protection via DMA from Untrustworthy Peripherals, NDSS 2019
- ↑ Muli Ben-Yehuda 외, The Price of Safety: Evaluating IOMMU Performance, Ottawa Linux Symposium 2007