IOTLB-SC 精读:IOMMU 的地址翻译缓存,也在向隔壁设备泄漏

设备要访问内存得先做地址翻译,翻译结果被 IOMMU 缓存进一块叫 IOTLB 的表里,而这表被挂在同一个 IOMMU 下的所有设备共用。于是控制一块可编程 PCIe 卡(比如 FPGA)的一方,只要盯着自己 DMA 访问的时延变化,就能知道隔壁那块设备——比如跑着 SQL 数据库的 GPU——有没有在干活,全程不需要任何内核权限。

论文:IOTLB-SC: An Accelerator-Independent Leakage Source in Modern Cloud Systems(ASIA CCS ’23,2023-07)

作者:Thore Tiemann、Thomas Eisenbarth(University of Lübeck)、Zane Weissman、Berk Sunar(Worcester Polytechnic Institute)

CPU 缓存侧信道已经被翻来覆去研究了很多年,但缓存并不只在 CPU 里。设备要访问内存也得先做地址翻译,翻译结果同样会被缓存起来,这块缓存叫 IOTLB,它属于 IOMMU,而且被挂在同一个 IOMMU 下的所有设备共用。这篇论文问的正是这件事:这块缓存会不会泄漏,泄漏出来的又是什么。

📚 基本概念速读

名称 定义 记忆点
虚拟地址 / 物理地址 程序里用的是虚拟地址,真正落到内存条上的位置叫物理地址 两者之间的换算就是地址翻译
页(page) 内存被切成大小固定的块,翻译和分配都以块为单位 常见 4 KB,大页 2 MB
MMU / TLB CPU 侧的地址翻译单元,以及它缓存翻译结果的地方 设备侧的对应物是 IOMMU / IOTLB
DMA(Direct Memory Access) 设备不经 CPU 直接读写主存(也就是内存条) 现代网卡、GPU、SSD 都靠它
IOMMU 挡在 PCIe 设备与主存之间的地址翻译与保护单元 CPU 里的 MMU 是给它做参考的样板
IOVA(I/O Virtual Address) 设备侧使用的虚拟地址 要和物理地址区分开
IOTLB IOMMU 里的地址翻译缓存,存下最近的 IOVA 到物理地址的映射 全文的主角,也是被共享的那块资源
Eviction set 一组地址,访问它们能稳定地把某个缓存组里的其他条目全挤出去 侧信道攻击的抓手
Prime+Probe 先用 eviction set 填满目标组,等一会儿,再访问一次并计时 不需要攻击者与受害者共享内存
Flush+Reload 先清掉目标缓存行(缓存与内存之间搬运数据的最小单位,通常 64 字节),等受害者执行,再读一次并计时 需要共享内存,但时间分辨率更高
PAC(Programmable Acceleration Card) Intel 的可编程加速卡,卡上是 FPGA 论文里攻击者用的就是它,云上可以按需租
ATS(Address Translation Services) 允许设备自己维护一份本地翻译表、绕过 IOMMU 翻译 论文的对策之一,也是双刃剑

🧭 起点:一块没人盯过的缓存

先分清两条地址翻译的路。程序里用的地址并不是内存条上的真实位置——操作系统给每个程序一套虚拟地址,真正落到硬件上的叫物理地址,两者之间的换算就是地址翻译。换算不是逐字节做的,而是按页来:内存被切成大小固定的块(常见 4 KB),页表记的是这一页对应哪一页物理内存。

CPU 访问内存时,这个换算由 MMU(内存管理单元)完成,算好的结果攒在 TLB 里,下次遇到同一个页就不必重算。设备那边有一模一样的一套:IOMMU 负责翻译,它的缓存叫 IOTLB。

两者的结构非常像,而且都不是一个大池子随便塞:一个地址先决定它落在哪一组(set),每组里只有固定的几个位置,这些位置叫路(way)。组被填满后还要放新条目进来,就得按替换策略挑一个旧的挤出去。但它们的暴露面完全不同:CPU 的 TLB 是每个核私有的,而 IOTLB 在硬件上就是一块被多个设备共用的表。

论文给了一个很具体的层次图:

1
2
3
4
5
6
地址翻译的两条路

CPU 侧: 应用程序 → MMU → TLB(每核私有) → 主存
设备侧: 设备 DMA → IOMMU → IOTLB(多设备共用) → 主存
↑
这条路上的缓存被共享

共享这件事本身不新鲜,CPU 缓存也是共享的。新的是这块缓存的位置:IOTLB 夹在设备与主存之间,任何走 DMA 的设备都得经过它,而它在此之前几乎没有被当成攻击面研究过。

论文还特别交代了它相对前人的假设更弱。PCIe 的拓扑是一棵树:CPU 这头先连到 root port(根端口),往下可以再接 switch 分叉出更多设备。此前有两项工作靠 PCIe 总线拥塞做侧信道,它们的前提是攻击者设备与受害者设备挂在同一个 PCIe switch 上;这篇只要求两者共享一个 root port。后一个条件更松:共享 switch 的设备一定共享 root port,反过来不成立。

🔬 怎么测出来:拿 FPGA 当秒表

想观测 IOTLB,得有一个能自己发 DMA 请求、还能精确计时的设备。CPU 做不到这件事——IOMMU 是给设备用的,CPU 没法直接去探它的状态。所以论文的观测工具就是一块 FPGA。

先要确认时延差真的存在。论文引了他人的测量:一次 IOTLB 未命中会让访问延迟增加约 330 ns——未命中意味着缓存里没有现成的翻译结果,得真去内存里翻页表、一级一级查下来。他们的 FPGA 跑在 200 MHz,换算下来期望看到 66 个时钟周期的差距(时钟周期是芯片最小的计时单位,200 MHz 就是每秒 2 亿个周期)。实测吻合:

配置 主存读延迟(FPGA 时钟周期)
关闭 IOMMU 160~185
开启 IOMMU,首次访问(翻译不在 IOTLB 里) 225~270
开启 IOMMU,后续访问(翻译已在 IOTLB 里) 与关闭 IOMMU 时接近
可测量的差值 65~85

65 到 85 个周期的差距相当充裕,足以稳定区分命中与未命中。论文在三套系统上验证过这一点,其中两套是云上的机器,只有普通用户权限。

硬件那一侧,论文写了一个叫 iotlb_pnp 的模块,可编程 7 条指令,实际支持 5 条:evset_prime、evset_probe、target_prime、target_probe、wait。顾名思义,前两条是访问一组 eviction set,中间两条是访问单个目标地址,wait 就让模块空转指定的周期数。其中 probe 类指令会数时钟周期——它是整套攻击的测量仪器。

另外还有一个只能在高权限环境用的辅助工具:一个内核模块,可以清空某个 IOMMU 的整个 IOTLB。加载内核模块需要 root,所以它只在作者能拿到 root 的那套本地机器上用了。

🎯 找 eviction set:不知道结构也能找

有了秒表,下一个问题是找到 eviction set——一组能可靠地把目标地址从 IOTLB 里挤出去的地址。

论文一开始的假设是 IOTLB 和传统 TLB 一样:地址说到底是一串二进制数,拿它最低的几位当组号,就能决定它落在哪一组。结果这个假设被实验推翻了(细节放在附录 A.1)。既然组织方式猜不出来,就只能用不依赖结构假设的办法。

他们提出的是 grow-reduce 算法,两步循环:

1
2
3
4
5
6
7
地址池(4096 个页)
↓ 随机挑地址加进候选集合,直到能稳定挤掉目标
确定能挤掉(grow)
↓ 逐个把地址拿出来试,拿掉之后还能挤掉,说明这个地址是多余的
只留必要的(reduce)
↓
eviction set:118 个地址,成功率 100%

这里有一个很实际的结论,和攻击者的权限直接相关:

是否清空 IOTLB 结果
每次 Prime+Probe 前清空 两套算法都能给出单个 118 地址的 eviction set,实测成功率 100%
不清空 没有算法能稳定给出这么规整的结果;此时 grow-reduce 的优势体现出来——它给出的集合更小、可靠性是 grow-split 的两倍

清空 IOTLB 需要内核权限,而云上的租户通常没有。也就是说,有权限的人能拿到近乎完美的 eviction set;没有权限的人得接受更粗糙的集合。论文因此把威胁模型按权限拆成了四档(1u/1k/2u/2k,u 表示只有用户权限、k 表示还有内核权限),主要分析的是无内核权限的那两档。

🕵️ 能看到什么:其实非常有限

这部分是全文最诚实、也最值得记住的地方。

论文搭的实验场景是:FPGA 与一块 NVIDIA Tesla T4 共处同一个 IOMMU,T4 上跑一个 GPU 加速的 SQL 数据库。攻击流程就是标准的 Prime+Probe:

1
2
3
4
5
6
7
1. Prime:FPGA 访问 eviction set,把 IOTLB 里那一组填满
↓
2. Wait:让 GPU 去跑一次 SQL 查询
↓
3. Probe:FPGA 再访问 eviction set 并计时
↓
变慢 → 有别的翻译挤进了这一组 → GPU 动过

结果是分层的,得一层层看:

第一层,能不能看见:能。GPU 跑查询时确实会在 IOTLB 里留下足迹,平均每次查询挤掉 18 到 20 个条目。

第二层,能不能区分查询内容:不能。论文特意挑了几个结果规模差别很大的查询(返回 0 行、1 行、409600 行),测量出来的 IOTLB 活动几乎一样。结果大的查询只会让痕迹持续更久,因为算得慢,痕迹的形状没有区别。

第三层,为什么区分不了:原因有两个,而且都来自当下的实现惯例:

  • 结果从 GPU 传回 CPU 时,走的是 MMIO(memory-mapped I/O:把设备的寄存器映射进内存地址空间,由 CPU 逐次读写)而不是 DMA。论文用 PCIe 性能计数器(CPU 里统计各类硬件事件发生次数的计数器)核实过:回传期间 IOTLB 一片安静,而 MMIO 读请求的量正好对得上返回结果的大小。地址翻译在 CPU 的 MMU 里做了,压根没走 IOMMU;
  • 常规的 DMA 是批量传输:把一大块数据搬进设备、算完再整块搬回去。整块搬运不产生与数据相关的访问模式,也就没什么可泄漏的。

把这两条合起来,论文给出的判断很直白:目前从 IOTLB 能稳定读到的基本上只有一位——隔壁那块加速器在不在干活。

不过两位作者没有停在这里,他们接着说了为什么这一位会变得越来越值钱:

  • PCIe 5.0 的速率已经逼近主存访问,设备厂商可能会因此减小设备本地内存,转而更频繁地访问主存;
  • CXL(Compute Express Link,架在 PCIe 5.0 物理层上的一套新互连协议,让 CPU 和加速器能像共享内存一样协作)带来的缓存一致性协议,会鼓励设备与主存之间更细粒度的数据交换,开发者会从批量传输转向数据相关的细粒度 DMA。

一旦这两件事发生,与数据相关的访问模式就会出现,而它正好落在 IOTLB 能看见的范围内。论文对自己的定位很清楚:现在不危险,但要赶在它变危险之前先把它讲出来。

📡 证明它真能通:两条隐蔽通道

上一节的结果是:攻击者能从 IOTLB 里读到一位信息——隔壁那台加速器动过没有。一位信息本身做不了什么,但只要攻击方能反过来主动控制这一位,它就成了一条信道:两侧约定好哪一段信号算 1、哪一段算 0,就能把消息一比特一比特地传出去。

这里要先分清两个容易混的概念,侧信道和隐蔽通道说的不是同一件事:

谁在传 传的是什么 本文里的角色
侧信道 受害者,且不知情 受害者的行为 跑着 SQL 查询的那块 GPU
隐蔽通道 两个串通的恶意方,主动发 他们自己的消息 发送方是外设或 CPU,接收方是 FPGA

隐蔽通道是一条合作的通道,它的价值不在偷数据,而在于证明这一位确实可控、可读、能稳定复现——这正是上一节停在只看到一位之后,需要补上的那一环。

这里值得停下来问一句:既然这条通道慢得几乎没法用来搬数据,搭它到底图什么?

因为侧信道的观测结果很容易被解释成巧合。攻击者探测到一次变慢,可能是隔壁设备干的,也可能是噪声、是别的设备、是测量抖动。要排除这些解释,最硬的证据就是反过来验证一遍:发送方按事先约定好的模式主动发信号,接收方解出来,看和发的是否一致。一致,就说明这条路径真实存在,而且闭环可控。

所以隐蔽通道在侧信道论文里的角色不是攻击手段,而是证明手段:

  • 侧信道:别人在发信号,我在读——读到的东西可能是巧合;
  • 隐蔽通道:我自己发信号、自己读回来——用来确认这条路真的通。

两者用的是同一套观测机制,只差谁在按键。能把自己选的一位传出去,就说明这一位是可控且可读的;那么反过来,受害者无意中产生的那一位,同样读得出来。

这里有个很容易冒出来的疑问:既然都能控制那台 GPU 了,直接动手不就行了,还测什么信道?关键在于,发送方扮演的是受害者,而不是攻击者。真实场景里,攻击者手里只有自己那块 FPGA,隔壁的 GPU 和数据库是别人的租户,他碰不到——他能做的只有读,而读到的信号对不对,他心里没有底,因为没人告诉他标准答案。所以论文先安排一个自己人假扮受害者:让那台 GPU 按已知的规律发信号,看接收端能不能一字不差地读懂。

这一步检验的是接收端的读能力,不是发送端的破坏力。读能力被确凿地验证之后,把同一个接收端对准真正的受害者,读到的就是真东西了。

那为什么不干脆用网络把消息传过去?因为那样测的就不是 IOTLB 了。这条通道的全部意义在于:整条消息只经过这一个共享硬件,中间没有别的路径——它证明的是这块硬件本身能不能承载信息。顺带说,在隔离很严、租户之间网络被切断的环境里,一条只走共享硬件的通路本身就有价值,所以它不只是实验手法。

论文搭了两条,区别在发送方是谁:一条由外设发,一条由 CPU 发;接收方都是那块 FPGA。

第一条:外设当发送方,跑一次查询编一个 1

既然 GPU 每跑一次 SQL 查询就会挤掉 IOTLB 里十几个条目,编码规则可以直接建在这上面:

  • 发送方跑一次查询 → 接收方探测到一片未命中 → 记作 1;
  • 发送方什么都不做 → 探测结果保持不变 → 记作 0。

论文实际传的是 Hello 这个词,图 4 里能看到这一串比特在时间轴上的形状。

这条通道的速率有一个很好算的来源:每一个 1 都要花掉一次 SQL 查询,而一次查询大约 0.3 秒。所以消息里 1 越多,传得越慢(下面表里的 bps 是比特每秒,1 字节 = 8 比特):

消息内容 吞吐 错误率
全是 1 3.4 bps 0%
1 与 0 各一半 6.65 bps 0%
全是 0 246.15 bps 0.1%
真实文本(ASCII) 7.58 bps 0%

真实文本那一行最能说明问题:Hello 是 5 个字节、40 个比特,按 7.58 bps 算要 40 ÷ 7.58 ≈ 5.3 秒;换成传一份 1 MB 的文件得花十几天。所以这条通道的价值是证明它成立,而不是拿来搬数据。

第二条:让 CPU 当发送方,靠刷 IOTLB

第二条反过来,由 CPU 发、外设收。这里有个前提得先讲清楚:CPU 自己是碰不到 IOTLB 的。IOMMU 只负责翻译外设的地址,CPU 访问内存走的是自己的 MMU 和 TLB,压根不经过 IOTLB。CPU 能影响它的方式只有两种——改页表项,或者直接命令 IOMMU 把条目刷掉。

而这两件事平时只有操作系统和 hypervisor 会做,所以 Linux 并不给用户态提供刷 IOTLB 的接口;论文的办法是自己写一个内核模块,把这个能力开出来。有了它,编码同样直白:

  • 传 1:CPU 刷一次整个 IOTLB,耗时约 17 µs;
  • 传 0:CPU 静默 17 µs,什么都不做;
  • 接收方还是那块 FPGA,它持续探测同一个固定地址——慢就是 1,快就是 0。

两边没做纠错,也没做严格同步(只是让 FPGA 按固定周期数等待),结果是 15023 bps,但错误率高达 30%。

两条放在一起看,结论比数字本身重要

通道 发送方要什么 吞吐 错误率
外设 → 外设(§6.1) 能控制一个外设即可 3.4~246 bps,取决于消息里有几个 1 ≤ 0.1%
CPU → 外设(附录 A.2) 内核权限(得先装内核模块) 15023 bps 30.09%

快的那条要内核权限,而且错误率高到必须纠错才能用;慢的那条几乎不出错,而且只要用户权限——它才落在论文真正的威胁模型里。这也解释了论文为什么把 CPU 那条放进附录:它更像一个可行性证明。

论文还试过把方向反过来(外设发、CPU 收),没成功:刷 IOTLB 的耗时和 IOTLB 里装着什么无关,稳定在 17 µs 左右,起不了信道作用。

🛡️ 怎么防

论文按现在能做和将来该做分了两层。

现在能做的:

  • 隔离:让互不信任的用户不要共用同一块 IOMMU 硬件。这是最彻底的一条,代价是牺牲扩展性——论文举了个很具体的例子:一块有 8 个满速 PCIe 插槽、但只有 4 块 IOMMU 的主板,没法配成 8 个彼此隔离的单卡实例,尽管物理上插得下 8 张卡;
  • 常量访问模式:让涉及秘密的计算以恒定的内存访问模式执行。密码学实现里这是常规操作,但数据库这类应用很难做到;
  • ATS:允许设备自己维护本地翻译表,翻译不走 IOMMU,也就不在 IOTLB 里留痕。但它只能开给可信设备——论文指出,恶意设备可以利用 ATS 把任意物理地址标成已翻译,从而绕开全部限制。

硬件层面该做的:让 IOTLB 也支持按组或按路分区——论文提到可以借鉴 Intel CAT(Cache Allocation Technology,CPU 上把末级缓存按路分配给不同进程的一项技术)的思路;或者干脆支持把某些页标记为不可缓存,让它永远不进 IOTLB。后一条的代价很直白——这些页的每一次访问都会像未命中一样慢。

⚠️ 这篇的边界

这篇论文的贡献和它的限制一样清楚,引用时最好一起带上:

位置 要注意的地方
当前泄漏量 论文自己写得很明白:现阶段能观测到的基本只有一位,即隔壁加速器是否在工作。它是一份未来威胁的报告,而不是一个能立刻偷数据的攻击
为什么现在不危险 两个当代事实撑着:结果回传走 MMIO(翻译在 CPU 侧做)、DMA 是批量传输(没有数据相关的访问模式)。这两条都是实现惯例,不是原理上的保证
未命中延迟的出处 330 ns 这个数字引自 Neugebauer 等人的 PCIe 性能基准测试,不是本文自己测的;本文测的是差值落在 65~85 个时钟周期
三套系统的差异 本地系统(a10l)作者有 root,云上两套(a10v、s10v)只有用户权限。所以侧信道与隐蔽通道实验只在 a10l 上跑,算法评估才覆盖三套
攻击者要有什么 一块可编程、能做 DMA 的 PCIe 设备(FPGA 或 GPU),且与受害者共享同一个 IOMMU。论文特意指出,AWS、阿里云这类平台已经允许租用带 DMA 能力的 FPGA,所以这个前提在云上是可达的
威胁模型编号 论文用的是 1u/1k/2u/2k 这套记号,后半的 u/k 表示攻击者是否另有内核权限。读的时候别把 k 档的结论套到 u 档上

✅ 总结

IOMMU 里的地址翻译缓存(IOTLB)是一块被多个设备共用的表,同一 IOMMU 下的另一块可编程 PCIe 设备能靠自己的 DMA 时延读出它的活动;但就目前而言,读到的基本只有隔壁设备在不在用这一位信息。

三个可以带走的要点:

  1. 共享缓存的清单比想象的更长。CPU 缓存、TLB 之外,设备侧的地址翻译缓存同样是共享资源,而且它挡在每一条 DMA 路径上;
  2. 威胁模型的门槛比想象中低。只需要一块和受害者共享 IOMMU 的可编程 PCIe 设备,不需要内核权限,也不需要与受害者共享内存——云上按需租一块 FPGA 就够了;
  3. 这篇的价值在于时点,而不是当下的杀伤力。作者反复强调 PCIe 5.0 与 CXL 会改变设备的访存习惯,把批量传输推向细粒度、数据相关的 DMA——而那正是这类侧信道真正需要的东西。先把话讲在前面,等条件成熟时它就不再是一个假说。