论文:Dynamic Detection of Vulnerable DMA Race Conditions-
作者:Brian Johannesmeyer, Raphael Isemann, Cristiano Giuffrida, Herbert Bos-
单位:Qualcomm Technologies, Inc. (San Diego) / Vrije Universiteit Amsterdam-
来源:ACM SIGSAC Conference on Computer and Communications Security (CCS) 2025-
原文:https://doi.org/10.1145/3719027.3765126
DMA:内核与外设之间的"共享黑板"
现代操作系统内核通过 DMA(Direct Memory Access)与外设高效通信。CPU 只需发起数据传输,DMA 控制器负责在系统内存和设备之间搬运数据,CPU 在此期间可以执行其他任务。这种设计极大提升了 I/O 性能,但也引入了一个安全问题:DMA 内存是内核和外设共享的资源。
Linux 内核中有两种 DMA 缓冲区。Streaming DMA 是异步的,驱动通过显式的同步操作在内核和设备之间切换访问权。Coherent DMA 是同步的,内核和设备可以同时访问这块内存,任何一方的写入对另一方立即可见。
问题就出在 coherent DMA 上。内核以为自己在读自己刚写入的数据,但恶意设备可能已经篡改了这些数据。这和用户态-内核态之间的 double-fetch 竞态条件本质上是一样的,只不过攻击者从恶意用户线程变成了恶意外设。
Linux 内核维护者 Greg Kroah-Hartman 在回应论文作者的漏洞披露时说:"我们过去几乎无条件信任硬件。内核里确实有一些代码在啃这块'到底信不信'的硬骨头,但从设计上来说,Linux 根本不是为这种威胁模型准备的。"
三种 DMA 竞态条件
论文定义了三种 DMA 竞态条件,分别违反三条不变式。
不变式 1:避免 TOCTOU bugs(Coherent DMA)。 内核不应在同一执行上下文中多次读取同一 coherent DMA 数据。如果第一次读取做了检查,第二次读取做了使用,恶意设备在两次读取之间篡改数据,就是 TOCTOU bug。这和用户态的 double-fetch 完全类似。
不变式 2:避免 TOITOU bugs(Coherent DMA)。 TOITOU = Time-of-Initialization To Time-of-Use。内核初始化 DMA 数据后,恶意设备在内核使用前覆盖了这些数据。内核加载时假设数据仍然有效,跳过了验证。这也是论文的核心发现,TOITOU 比 TOCTOU 更危险也更常见。内核经常在 DMA 中管理整个数据结构(如链表),设备篡改链表节点中的指针后,内核使用被污染的指针就是 TOITOU bug。
不变式 3:避免 inconsistent access bugs(Streaming DMA)。 Streaming DMA 区域同步给设备访问时,内核不应访问它。因为此时 CPU cache 或 bounce buffer 中的数据可能与实际 DMA buffer 不一致。这类问题主要是可靠性 bug,恶意设备难以利用。
DMARacer 的检测流程
DMARacer 由四个组件构成,协同工作来检测 DMA 竞态条件。

DMARacer检测流程
组件 A hook 所有底层 DMA API(dma_alloc_attrs、dma_map_single_attrs 等),追踪 DMA 区域的位置和状态。组件 B 监控所有内存访问指令,检查是否访问 DMA 区域以及是否违反不变式。组件 C 用动态污点追踪(DTA)跟踪 errant access 中攻击者可控的数据流,到达安全敏感操作时报告为 vulnerable operation。组件 D 通过 QEMU 虚拟设备模拟和定制 workload 来覆盖 DMA 代码路径。
在 Figure 1 的示例中,驱动代码分配了一块 coherent DMA(dma_ptr),随后两次读取 *dma_ptr,第一次做大小检查,第二次赋值给 x。第二次读取是 TOCTOU bug,因为恶意设备可以在两次读取间修改数据。此后,x 的值被用作数组索引执行写入 arr[x] = 0xdeadbeef,这是一个 attacker-controllable memory write,被标记为 vulnerable operation。

DMARacer工作流
DMARacer 的实现基于 KDFSAN(Kernel DataFlowSanitizer),用 LLVM 在编译时插入 LOAD/STORE/MEMTRANSFER/AND/BranchInst 等指令级别的回调。运行时,被插桩的内核在定制 QEMU 中执行,输出执行日志和 DMA 竞态条件统计。
污点策略:在精确性和可扩展性之间取舍
DMARacer 的污点策略需要在精确性和可扩展性之间找平衡。Source 是 TOCTOU/TOITOU bug 的输出,攻击者可控的数据。Sink 包括三类安全敏感操作:内存写的指针(可实现任意写原语)、循环条件(可导致死循环或缓冲区溢出)、断言条件(可触发 kernel panic 导致 DoS)。
防止 taint explosion 是 DTA 系统的经典难题。DMARacer 采用了三种保守的清除策略。AND 指令清除 taint,因为位级污点追踪已知非常困难,而内核经常用 AND 操作提取 DMA 数据中的个别位(如 1-bit 标志)。内核热路径(timer 子系统、slab 分配器等频繁访问全局数据的代码)清除 taint,否则污点会迅速扩散到无关操作。执行上下文切换时标记 taint 为 invalid,确保只有同一执行上下文内的数据流才被报告为 vulnerable operation,保证结果可重现。
这些保守策略可能引入假阴性,例如依赖位级数据流的漏洞可能被漏报。但论文认为这比 over-tainting 导致的假阳性更好,因为假阳性会削弱工具的可信度。
实验结果:817 个错误访问,344 个可利用操作
论文在 Linux v6.5.8 上评估 DMARacer,使用 147 个 QEMU 虚拟设备(36 个键鼠、61 个网卡、25 个存储设备、13 个声卡、12 个 GPU),每个设备运行约 5 分钟的定制 workload。

DMA竞态条件检测结果
Table 3 结果。总计发现 817 个 errant accesses 和 344 个 vulnerable operations,分布在 79 个 DMA 区域中。Streaming DMA 有 576 个 errant accesses(其中 VMXNET3 驱动贡献了 455 个),Coherent DMA 有 241 个 errant accesses 导致了 312 个 vulnerable writes、22 个 vulnerable loops 和 10 个 vulnerable asserts。
VMXNET3 驱动贡献了 455 个 streaming DMA errant accesses,这源于一个单一的 API 误用。USB host 驱动有 119 个 coherent DMA errant accesses 和 18 个 vulnerable loops。block 层有 32 个 vulnerable writes 但没有 errant accesses,说明这些 vulnerable writes 的污点来源在别处,数据流跨越了子系统边界。
errant accesses 的假阳性率为 0%,因为 DMARacer 追踪的是对真实 DMA 区域的具体内存访问,没有近似。vulnerable operations 的假阳性率为 9%,主要来自未建模的数据流约束,例如 ALSA 音频驱动中,内核从 DMA 读取索引值返回给 PCM 子系统,PCM 层对其做了边界检查并重置为 0,虽然数据被污染但实际访问是安全的。
TOITOU 比 TOCTOU 更危险
论文最重要的分析之一是 TOITOU 和 TOCTOU 的对比。在所有 errant accesses 中,57% 是 TOITOU bugs,43% 是 TOCTOU bugs。但 99% 的实际漏洞依赖 TOITOU 加载的数据。
原因在于数据结构的复杂度。TOCTOU bug 通常涉及简单的数据,比如多次读取一个状态标志。如果攻击者篡改了状态标志,影响通常有限,因为标志不控制关键操作(如内存写的指针)。TOITOU bug 则通常涉及内核在 DMA 中管理的复杂数据结构,尤其是链表。当内核从 DMA 中的链表加载指针时,它加载的是一个攻击者完全可控的指针。一旦内核解引用这个指针(比如遍历链表),就执行了一次 attacker-controllable memory access。
TOITOU 更危险还有两个原因。第一,它涉及长生命周期数据,内核可能在 boot 时初始化 DMA 数据结构,很久之后才使用,攻击窗口非常大。TOCTOU 则要求攻击者在两次读取的极短时间窗口内完成篡改,需要精确的时序同步。第二,TOITOU 更难修复,它影响关键数据结构,可能需要大规模重写驱动的算法逻辑;TOCTOU 通常可以通过合并两次读取为一次来局部修复。
案例一:DMA Pool API 的"堆腐蚀"
DMA pool 是 Linux 内核中广泛使用的内存管理机制,类似于传统堆:它从一块大的 coherent DMA buffer 中分配小块,用链表管理空闲块。

DMA Pool利用链
Figure 4 展示了 DMA pool 的利用过程。步骤 1,DMA pool 初始化链表并指向第一个块。步骤 2,恶意设备覆盖第一个块的 next_block 指针,使其指向一个任意内核地址 kernel_addr。步骤 3,驱动调用 dma_pool_alloc(),pool 本应指向第二个块,但实际指向了 kernel_addr。步骤 4,驱动再次调用 dma_pool_alloc(),返回 kernel_addr,此后驱动对这块"块"的任何写入,实际上都在腐蚀敏感内核数据。
这和传统堆元数据腐蚀攻击(如 Phrack 2001 年的 "once upon a free()")在结构上完全类似。区别在于,传统堆腐蚀是恶意用户态程序覆盖堆元数据,而 DMA pool 腐蚀是恶意设备通过 DMA 覆盖链表指针。
由于 DMA pool API 在内核中被广泛使用,每个使用它的驱动都可能受影响。论文作者提交了修复方案,但被拒绝了,理由是 API 变更太大、性能影响不可接受。这说明这类问题难以简单修复。
案例二:跨子系统的数据流——从 E100 驱动到 swiotlb
第二个案例展示了 DMARacer 相比静态分析的核心优势:跨子系统数据流追踪。
Intel E100 网卡驱动在初始化时分配了一块 coherent DMA 区域。在另一个系统调用中,驱动将 streaming DMA 映射返回的 bus address 存入这块 coherent DMA,这意味着这个地址现在是攻击者可控的。之后在中断处理中,驱动从 DMA 读取这个地址传给 dma_unmap_single()。dma_unmap_single 最终调用到 swiotlb 子系统中的 swiotlb_release_slots(),其中有一个 BUG_ON 断言检查 aindex < nareas,如果攻击者篡改了 DMA 中的地址,可以触发这个断言导致 kernel panic。
这个漏洞的检测难度在于,数据流跨越了三个子系统:驱动代码(e100.c)、DMA 映射 API、swiotlb。涉及多次间接调用(通过函数指针),异步事件(中断),以及跨执行上下文的数据流(从一个系统调用到另一个,再到中断处理)。静态分析工具 SADA 明确表示不分析函数指针调用,因此无法构建完整的调用图,会遗漏这类跨子系统问题。DMARacer 的动态方法在运行时追踪真实的控制流和数据流,能够发现这类问题。
论文提出了一个有意思的责任归属问题:应该谁来修?驱动不应该把 bus address 存到攻击者可控的 DMA 区域;但 swiotlb 也不应该用 kernel panic 来处理错误输入。作者建议两端都需要修复。这个案例已经在近期内核版本中被修复。
案例三:VMXNET3:一个误解导致数百个 bug

VMXNET3驱动不一致访问
VMXNET3 是 VMware 的高性能网卡驱动。Figure 6 展示了一个令人震惊的案例。在 vmxnet3_probe_device() 函数中,驱动在 boot 时将整个 adapter 结构体映射为 streaming DMA。映射操作 dma_map_single 返回后,在同一行代码上,驱动立即将返回值写入 adapter->adapter_pa 字段,但此时该区域已经同步为设备可访问,所以这个写入就是一个 inconsistent access。
此后在同一函数内,驱动还执行了 52 次对这个 adapter 对象的访问,每次都是 inconsistent access,因为该区域在映射后从未被同步回 CPU 可访问。加上其他函数中的访问,VMXNET3 驱动贡献了 455 个 streaming DMA errant accesses——全部源于开发者对 DMA 同步规则的一个基本误解:认为映射后立即访问是安全的。
论文分析了所有 579 个 streaming DMA errant accesses,发现只有 10 个是同步操作后发生的,其余 569 个是映射操作后直接发生的。这表明问题不是个别驱动的疏忽,而是开发者对 streaming DMA 同步规则存在系统性的认知偏差。VMXNET3 案例被报告给开发者后未收到回复。
性能开销与覆盖率
DMARacer 的平均运行时开销为 402%(LMBench),与 MemorySanitizer(200%-400%)相当,好于 ThreadSanitizer(400%-1400%)。开销主要来自两个部分:KDFSAN 污点追踪占 194%,load 指令监控占 139%。DMA 区域追踪本身几乎零开销,因为只是在 DMA API 调用时更新元数据,不需要检查每条指令。
覆盖率方面,Linux 内核共有 1631 个 DMA 映射调用位点,DMARacer 覆盖了 68 个(4.2%),其中 37 个(54.4%)受竞态条件影响。覆盖率主要受限于 QEMU 可模拟的设备数量,147 个虚拟设备中有许多使用相同驱动,共享映射位点。
54.4% 的受影响比例非常高,说明 DMA 竞态条件不是个别 bug,而是驱动代码中的系统性问题。
比静态分析强在哪里
论文与 SADA(USENIX Security 2021)做了间接对比。SADA 用静态分析检测 unsafe DMA accesses,包括 TOCTOU bug 和其他 errant access。但 SADA 有几个根本性限制:不分析函数指针调用,无法构建完整调用图;无法处理异步完成和中断驱动的控制流;无法追踪跨子系统的数据流。
SADA 没有公开代码,论文作者通过搜索 Linux 内核邮件列表中的报告和补丁来分析 SADA 的发现。结果显示,SADA 报告的 vulnerable operations 通常局限在单个函数内,而 DMARacer 发现的案例(如 E100→swiotlb 跨三个子系统)远超 SADA 的分析能力。
DMARacer 的动态方法也有其代价:需要设备模拟(QEMU)或物理设备,覆盖率取决于可模拟/可连接的设备数量;无法发现未被执行代码中的 bug;taint 空间有限(256 色),可能混淆独立执行路径。但论文认为,动态分析的跨子系统追踪能力是不可或缺的,DMA 竞态条件的危害恰恰在于数据流跨越了驱动、映射 API、文件系统、网络栈等多个子系统。
几点思考
DMA 安全是系统性问题。 817 个 errant accesses 和 344 个 vulnerable operations 分布在 79 个 DMA 区域、20 多个内核子系统中。三个案例研究中,只有一个被修复(E100→swiotlb),DMA pool API 的修复因 API 变更太大被拒绝,VMXNET3 未收到回复。这说明 DMA 安全问题不是修几个 bug 就能解决的,需要系统级的设计变更。
TOITOU 是一个被忽视的 bug 类别。 内核在 DMA 中初始化数据后,设备可以在内核使用前篡改,这种模式的普遍性远超预期。99% 的实际漏洞依赖 TOITOU 数据,但此前的研究主要关注 TOCTOU。TOITOU 涉及长生命周期数据和复杂数据结构,比 TOCTOU 更容易利用也更难修复。
DMA 编程模型对开发者来说过于复杂。 569/579 个 streaming DMA 错误是因为映射后未同步就访问。VMXNET3 驱动的开发者在映射操作的同一行就犯了错误。这表明 DMA API 的设计可能需要改进——如果正确使用 API 的难度如此之高,那么 API 本身可能就是问题的一部分。