CubeSandbox Hypervisor 模糊测试实战指南:基于 cargo-fuzz 的组件级 Fuzzing 体系
2026/9/16 16:38:21 网站建设 项目流程

CubeSandbox Hypervisor 模糊测试实战指南:基于 cargo-fuzz 的组件级 Fuzzing 体系

【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox

导读

本文围绕 CubeSandbox 仓库内嵌的 Cloud Hypervisor 虚拟化层(hypervisor/)所采用的模糊测试(Fuzzing)实践展开,系统讲解其基于 cargo-fuzz 建立的组件级模糊测试体系:如何准备工具链、如何运行现有的 13 个 fuzzer、如何阅读和改造 fuzz target 以覆盖 virtio 设备、legacy 设备、磁盘镜像格式与 HTTP 管理 API。读完本文,你将掌握在 Rust 虚拟化项目中搭建、运行、扩展模糊测试的完整方法,并能直接复用仓库中hypervisor/fuzz/fuzz_targets/下的成熟模板为自己的组件编写 fuzzer。

为什么虚拟化层需要模糊测试

虚拟化软件运行在宿主机的最底层,直接处理来自 guest 的 I/O 请求、设备模拟状态机与磁盘镜像文件解析,任何越界读写、整数溢出或状态机异常都可能演变为宿主机内核空间的漏洞,甚至破坏多租户隔离。这正是 CubeSandbox 所依赖的 Cloud Hypervisor 内核将模糊测试作为持续集成一部分的原因:用自动生成的畸形输入去冲击设备模拟和文件解析代码,在崩溃变成漏洞之前把它找出来。

在仓库中,这套模糊测试体系位于 hypervisor/fuzz/,由两大部分组成:

  • fuzz_targets/目录:存放全部 13 个独立的 fuzz target 源文件(balloon.rsblock.rscmos.rsconsole.rshttp_api.rsiommu.rsmem.rspmem.rsqcow.rsrng.rsserial.rsvhdx.rswatchdog.rs);
  • Cargo.toml:一个独立的 cargo-fuzz 工程清单,为每个 fuzz target 声明对应的[[bin]]入口。

Cargo.toml中可以看到,这个 fuzz 工程直接以路径依赖的方式引用仓库内的vmmdevicesvirtio-devicesqcowvhdxblock_utilvm-memory等核心 crate,并通过libfuzzer-sys = "0.4.5"接入 libFuzzer 引擎,同时依赖micro_http(HTTP 请求解析)用于 API 层模糊测试。

准备工作:切换到 nightly 工具链并安装 cargo-fuzz

cargo-fuzz 依赖 libFuzzer 的 unstable 特性,因此必须使用 nightly 版本的 Rust 工具链。按照 hypervisor/docs/fuzzing.md 的步骤:

# 1. 在 hypervisor/ 目录下将工具链切换为 nightly rustup override set nightly # 2. 安装 cargo fuzz 子命令 cargo install cargo-fuzz

两点说明:

  • rustup override set nightly只在当前目录(及其子目录)生效,不会影响系统全局的默认工具链,这是刻意为之——项目日常构建仍应使用稳定版工具链,只有运行 fuzzer 时才需要 nightly;
  • cargo install cargo-fuzz会从 crates.io 拉取并编译 cargo-fuzz 及其依赖,耗时与机器性能相关。若网络受限,可考虑使用cargo install --locked cargo-fuzz固定依赖版本以获得可复现的构建。

仓库的 hypervisor/Makefile 与 hypervisor/Cargo.toml 均将 fuzz 工程排除在主工作区之外(Cargo.toml中通过独立的[workspace] members = ["."]声明,避免与上层 workspace 互相干扰),因此运行 fuzzer 前需要先进入 fuzz 目录,或者使用--manifest-path指定清单路径。

运行现有 fuzzer:以 block 为例

文档给出的运行方式是直接用cargo fuzz run,并将nproc得到的 CPU 核数传给-j做并行:

cargo fuzz run block -j `nproc`

这条命令的完整语义是:以 libFuzzer 引擎编译并启动blockfuzzer,并行使用所有可用 CPU 核心。命令执行后,libFuzzer 会持续生成输入、喂给 hypervisor/fuzz/fuzz_targets/block.rs 中的fuzz_target!闭包,一旦触发 panic、越界访问或断言失败,就会把触发崩溃的最小输入保存到fuzz/artifacts/block/目录,供后续复现与修复。

语料库(corpus)与输入规模控制

libFuzzer 会把 fuzz target 运行期间"有趣"的输入保存到fuzz/corpus/<fuzzer名>/作为种子语料。对于需要较大输入的 target,可以显式限制单次输入的最大长度,例如 vhdx.rs 头部注释中给出的运行方式:

cargo fuzz run vhdx -j 32 -- -max_len=16777216

该注释还提供了语料库的生成方法——VHDX 是微软的虚拟磁盘格式,直接随机字节很难命中有效的文件头,因此先用真实工具生成一个合法样本再放入 corpus:

# 生成 16MB 的空文件并转换为 vhdx 格式 truncate -s 16M /tmp/source qemu-img convert -O vhdx /tmp/source fuzz/corpus/vhdx/test.vhdx # 再以该样本为种子运行,限制输入最大 16MB cargo fuzz run vhdx -j 32 -- -max_len=16777216

这是"种子语料 + 规模约束"的标准组合拳:种子保证变异起点接近合法输入、能够深入解析路径,-max_len防止输入无限膨胀拖垮执行速度。

13 个 fuzzer 一览:覆盖三大攻击面

结合 hypervisor/fuzz/Cargo.toml 中声明的全部[[bin]]入口,当前 fuzz 体系覆盖了虚拟化栈的三大攻击面:

fuzzer攻击面入口文件
blockvirtio-block 磁盘设备(同步 raw 文件后端)fuzz_targets/block.rs
balloonvirtio-balloon 内存气球设备(3 队列)fuzz_targets/balloon.rs
consolevirtio-console 控制台设备(2 队列 + 管道输入)fuzz_targets/console.rs
iommuvirtio-iommu 设备(请求队列/事件队列)fuzz_targets/iommu.rs
memvirtio-mem 热插拔内存设备fuzz_targets/mem.rs
pmem持久内存(virtio-pmem 相关)fuzz_targets/pmem.rs
rng随机数设备fuzz_targets/rng.rs
watchdog看门狗设备fuzz_targets/watchdog.rs
cmoslegacy CMOS(RTC)设备读写fuzz_targets/cmos.rs
seriallegacy 串口设备读写fuzz_targets/serial.rs
qcowQCOW 镜像格式解析与写入fuzz_targets/qcow.rs
vhdxVHDX 镜像格式解析与读写fuzz_targets/vhdx.rs
http_apiVMM HTTP 管理 API(全部路由)fuzz_targets/http_api.rs

可以看到,fuzz 目标不是随意挑选的,而是遵循"guest 可控输入直达代码"的原则:virtio/legacy 设备接收 guest 写入的队列描述符与端口字节流,磁盘格式解析器接收用户提供的镜像文件,HTTP API 接收宿主机侧的网络请求——这三类输入一旦畸形,最容易暴露未校验的边界。

深入源码:四类 fuzz target 的典型实现模式

虽然 13 个 fuzzer 目标各异,但实现上呈现出清晰的模式。读懂这些模式,是编写新 fuzzer 的起点(文档也指出设计灵感可参考 crosvm 的 fuzz 目录,其采用了高度相似的fuzz_target!宏与"桩设备+假中断"手法)。

模式一:virtio 设备——"假中断 + 内存注入"

virtio 设备是数量最多的 fuzz 目标。以 block.rs 为例,其做法极具代表性:

  1. 输入切分:把 fuzz 输入分为两段——前 4 字节QUEUE_DATA_SIZE用作 virtio 队列的"控制参数",剩余字节直接作为 guest 物理内存内容写入;
  2. 构造真实设备:通过memfd_create创建匿名内存文件作为磁盘后端,构造Block::new(...)实例(SeccompAction::Allow表示 fuzz 场景不启用 seccomp 过滤,EventFd::new(EFD_NONBLOCK)提供中断通知 fd);
  3. 注入畸形队列状态setup_virt_queue用输入字节分别设置next_availnext_usedevent_idx与队列size,构造出"半合法"的 vring 状态,随后把 desc/avail/used 三个 ring 布局在固定的 guest 物理地址;
  4. 触发处理路径:先向队列 eventfd 写入 1(模拟 guest kick),再调用block.activate(...)激活设备,最后wait_for_epoll_threads()等待设备工作线程处理完所有队列事件。

贯穿全程的NoopVirtioInterrupt(实现VirtioInterrupt::trigger直接返回Ok(()))是关键设计——fuzz 场景不需要真正的中断注入,用一个空操作桩代替即可让设备代码跑完整个"取描述符→校验→处理→回写 used ring"的路径。

balloonconsoleiommumem遵循同一模板,差异在于队列数量与内存布局:

  • balloon.rs 处理 3 个队列(inflate / deflate / reporting),分别用EventFd模拟,并各自 kick 一次后再激活;
  • console.rs 处理输入/输出 2 个队列,额外用pipe2创建管道,将 fuzz 输入的前 128 字节作为"guest 控制台输入"写入管道写端,覆盖设备从管道读数据的分支;
  • iommu.rs 用同一份队列数据同时构造 request queue 与 event queue(当前 virtio-iommu 实现不处理事件队列),并把 IOVA 空间范围作为参数传入设备;
  • mem.rs 通过MemoryManager::create_ram_region为 virtio-mem 构造 RAM 区域,用输入字节的第 1 位决定是否带 NUMA id,覆盖 NUMA 相关分支。

模式二:磁盘格式解析——"纯解析 + 写入验证"

与 virtio 设备不同,qcow.rs 与 vhdx.rs 不构造任何设备,直接攻击镜像格式解析器。

qcow fuzzer 的输入布局很有意思:前 16 字节被拆成两个 u64——前 8 字节作为"写入地址",后 8 字节作为"待写入数据",剩余字节整体作为 QCOW 镜像内容写入 memfd。随后尝试用QcowFile::from(RawFile::new(...))打开这个"畸形镜像",如果解析成功(Ok),就seek到指定地址并写入 8 字节数据。这样一次运行同时覆盖了镜像元数据解析集群映射/数据写入两条路径——而 QCOW 的集群分配逻辑恰恰是最容易出整数溢出的地方。

vhdx fuzzer 则采取"先整读再整写"策略:将全部输入字节写入 memfd 作为 VHDX 文件,Vhdx::new解析成功后,按 8192 字节块从头读到尾,再从头写一遍。注意它使用了read_exact(...).ok()write_all(&data).ok()——忽略 I/O 错误,只关心解析与读写过程中是否触发内存安全问题。

模式三:legacy 设备——"端口读写序列"

cmos.rs 与 serial.rs 针对 legacy(非 virtio)设备。这类设备的输入不是内存,而是总线端口访问序列,因此 fuzz 输入被解释为"操作码流":

  • cmos:前 16 字节分别构造 4GB 以下/以上的内存大小,随后每 2 字节一组,第 1 字节决定读/写(偶数读、奇数写),第 2 字节决定偏移(% 2映射到 CMOS 的两个 I/O 端口),写入数据取自下一个字节;
  • serial:每 3 字节一组,% 3决定动作——读端口、写端口、或queue_input_bytes模拟串口输入,端口偏移取% 8(覆盖 8 个 16550 UART 寄存器)。

这种"输入即指令序列"的编排方式,让 libFuzzer 的变异器可以自然地发现能走到深层状态机(如 FIFO 溢出、中断触发)的端口访问序列。

模式四:HTTP API——"路由选择 + 请求伪造"

http_api.rs 是唯一针对管理面的 fuzzer,攻击的是宿主机侧的 VMM HTTP 服务。它的设计包含两个值得借鉴的细节:

  1. 可复现性优先:用once_cell::sync::Lazy静态收集HTTP_ROUTES.routes.values()得到路由列表,保证每次进程启动时路由顺序确定(注释明确说明这是为了测试用例可复现);
  2. 桩接收器:fuzz 输入第 1 字节决定命中哪条路由,剩余字节构造 HTTP 请求(前 1 字节% 5决定 GET/PUT/PATCH/POST/INVALID 方法,其余字节作为请求体并拼上Content-Length头)。handle_request派发的ApiRequest通过 channel 传给一个专用线程的http_receiver_stub——该桩线程用epoll等待事件,对所有可能的ApiRequest变体(VmCreateVmBootVmSnapshotVmAddDeviceVmSendMigration等数十种)一律回送Ok(ApiResponsePayload::Empty)

这里的"全路由遍历 + 桩接收"模式,使得 fuzzer 可以在不真正启动 VMM 的情况下,遍历所有 API handler 的参数解析与请求校验代码。

添加新的 fuzzer:标准流程

文档给出了新增 fuzzer 的入口命令:

cargo fuzz add <new_fuzzer>

该命令会在fuzz/fuzz_targets/下生成一个模板文件(默认内容是一个把输入字节追加到 Vec 的占位fuzz_target!),并在 hypervisor/fuzz/Cargo.toml 中自动追加对应的[[bin]]声明。之后要做的,是把占位实现替换为上述四类模式之一。以新增一个 virtio 设备 fuzzer 为例,最小骨架如下:

#![no_main] use libfuzzer_sys::fuzz_target; fuzz_target!(|bytes| { // 1. 对输入做长度/规模前置校验,避免无效输入浪费执行时间 // 2. 参考 block.rs 的模板:切分输入 -> 构造设备(SeccompAction::Allow)-> 注入队列状态 -> kick eventfd -> activate -> wait_for_epoll_threads() });

从源码可以归纳出编写 fuzz target 的几条经验:

  • 无副作用优先:所有设备后端都使用memfd_create创建的内存文件(如 block.rs 的memfd_create辅助函数),绝不触碰真实磁盘,使 fuzz 进程可以无状态、高频率地执行;
  • 前置长度过滤:每个 fuzz target 开头都有一组bytes.len() < ... || bytes.len() > ...的范围检查,把输入约束在能覆盖关键路径的最小窗口内,避免执行时间被超大输入拖垮;
  • 宽度优先的桩NoopVirtioInterrupthttp_receiver_stub证明,fuzz 的目标是"被测试代码的解析与状态处理逻辑",与测试目标无关的依赖一律用最小桩替代。

崩溃复现与回归

cargo fuzz run发现崩溃时,libFuzzer 会把触发崩溃的最小输入写入fuzz/artifacts/<fuzzer名>/。复现时直接对该文件重放即可:

# 在 hypervisor/fuzz 目录下 cargo fuzz run block fuzz/artifacts/block/crash-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

对于从崩溃输入中提炼出的回归样本,可以把它作为种子放入 corpus 目录,让后续所有 fuzz 会话都从该输入起步,确保同一条路径每次都被覆盖,防止回归。

适用前提与边界

最后澄清几个容易误用的点:

  • 运行环境:cargo-fuzz 目前仅支持 Unix 系平台(libFuzzer 依赖fork/dev/shm等机制),Windows 上无法运行;本文命令默认在 Linux/macOS 的 bash 环境中执行;
  • 工具链要求:必须使用 nightly 工具链,且rustup override set nightly仅对当前目录生效,不会污染项目的稳定版构建;
  • 不要在生产环境运行 fuzzer:fuzz target 中的设备以SeccompAction::Allow构造,且绕过了正常 VMM 的初始化流程,其目的只是暴露内存安全缺陷,不代表可用的虚拟化功能;
  • 这是模糊测试代码而非生产功能hypervisor/fuzz/是一个独立的 cargo-fuzz 工程,不参与 CubeSandbox 主程序的构建产物。

总结

CubeSandbox 内嵌的 Cloud Hypervisor 以 hypervisor/docs/fuzzing.md 为入口,建立了一套覆盖 virtio 设备、legacy 设备、磁盘镜像格式与 HTTP 管理 API 四大攻击面的组件级模糊测试体系。其方法论可归纳为:nightly 工具链 + cargo-fuzz 引擎 + 无副作用的 fuzz target 模板 + 最小桩依赖。无论是想为新的虚拟设备补一个 fuzzer,还是理解 Rust 虚拟化项目如何做持续安全测试,hypervisor/fuzz/fuzz_targets/ 下的源码都是可以直接参照的活教材。

【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询