首页
/ OpenConsole 模糊测试指南:用 Fuzzing 构建配置与 OneFuzz 对 Windows 控制台宿主做 LibFuzzer 模糊测试

OpenConsole 模糊测试指南:用 Fuzzing 构建配置与 OneFuzz 对 Windows 控制台宿主做 LibFuzzer 模糊测试

2026-09-05 13:32:32作者:劳婵绚Shirley

本篇基于 doc/fuzzing.md 展开,讲解 OpenConsole(Windows Terminal 与 Windows 控制台宿主 conhost 的统一仓库)如何接入 LibFuzzer 模糊测试体系:如何在本地以 Fuzzing 配置构建出可运行的模糊测试目标(fuzz target)、LLVMFuzzerTestOneInput 入口函数如何驱动 conhost 的核心缓冲区写入路径,以及如何用 OneFuzz 把模糊测试任务跑在 CI 上并配置缺陷告警。读完本文,你能独立完成一次本地 fuzzer 构建与运行,并理解 Fuzzing 构建配置中 ASAN、覆盖插桩与 vcpkg triplet 的协作关系。

模糊测试在 OpenConsole 中的定位

OpenConsole 是 conhost(Windows 控制台宿主)与 Windows Terminal 的合并代码库,其核心输入路径——控制序列解析、缓冲区写入——长期暴露给任意用户与外部程序产生的字节流,是典型的模糊测试目标。仓库为此提供了一条完整的工具链:

  • 本地模糊测试:解决方案内置 Fuzzing 构建配置,产物是一个自带 LibFuzzer 驱动的可执行程序(fuzzer 可执行文件),对给定的测试用例(corpus 文件)反复变异、注入;
  • CI 模糊测试:通过微软的 OneFuzz 服务在持续集成中长时间运行 fuzzer,并把新发现的 bug 通过通知系统(MS Teams、Azure DevOps 工作项)推送给开发者。

本地与 CI 共用同一套构建产物,因此本地配置好 Fuzzing 构建是理解整条链路的前提。

Fuzzing 构建配置:ASAN、覆盖插桩与静态链接

仓库在所有 C++ 项目上通过 src/common.build.pre.props 声明了 Fuzzing 配置(Fuzzing|Win32Fuzzing|x64Fuzzing|ARM64 三种平台组合),该配置下的关键编译/链接行为如下(见 src/common.build.pre.props 第 248–264 行附近):

项目 设置 作用
编译选项 /fsanitize=address /fsanitize-coverage=inline-bool-flag /fsanitize-coverage=edge /fsanitize-coverage=trace-cmp /fsanitize-coverage=trace-div 启用地址消毒剂(ASAN)与覆盖插桩(边覆盖、比较追踪、除法追踪),让 fuzzer 知道哪些分支被执行过、哪些比较值值得探索
CRT 链接 RuntimeLibrary = MultiThreaded(静态 CRT) LibFuzzer 要求静态运行时,避免运行时库与 ASAN 注入冲突
预处理器宏 FUZZING_BUILD 源码据此切换入口函数(main() 还是 LLVMFuzzerInitialize
链接依赖 libsancov.libclang_rt.asan_dynamic-<arch>.lib 挂接 ASAN 动态库;架构名通过 OCClangArchitectureName 映射(x64 → x86_64,x86 → i386

由于全静态链接,vcpkg 侧也需要配合。Fuzzing 配置会额外传入 overlay triplet(src/common.build.pre.props):

--overlay-triplets=$(SolutionDir)\dep\vcpkg-overlay-triplets\fuzzing

对应 triplet 文件 dep/vcpkg-overlay-triplets/fuzzing/x64-windows-static.cmake 在官方 x64-windows-static triplet 基础上做了两点强化:

# Same as the official x64-windows-static triplet
set(VCPKG_TARGET_ARCHITECTURE x64)
set(VCPKG_CRT_LINKAGE static)
set(VCPKG_LIBRARY_LINKAGE static)

# ...but with explicit platform toolset, so that future toolsets
# aren't automatically picked up (it defaults to the latest one).
set(VCPKG_PLATFORM_TOOLSET v145)

set(VCPKG_CXX_FLAGS /fsanitize=address)
set(VCPKG_C_FLAGS /fsanitize=address)

即第三方依赖(第三方 C/C++ 库)也以 ASAN 编译并静态链接,保证依赖内部触发的内存错误同样能被捕获;同时显式锁定平台工具集版本,避免将来新版工具集自动生效导致的构建漂移。Fuzzing 配置还把 vcpkg 安装目录独立为 obj\$(Platform)\vcpkg-fuzzing,与普通构建的 vcpkg 目录隔离,避免两套依赖互相污染。

从源码结构看,个别子项目在 Fuzzing 下还会改变产物形态:例如 src/host/proxy/Host.Proxy.vcxprojConfigurationType 从 DLL 改为 StaticLibrary——注释说明其原因是该代理 DLL 在 Fuzzing 构建中并非模糊测试目标,强行产出可用 PE 会失败,改成静态库既能参与链接又绕开该问题。

本地设置 fuzzer

OpenConsole 可以以 Fuzzing 配置构建。要接入一个 fuzzer,核心是提供 LLVMFuzzerTestOneInput 函数——它充当 LibFuzzer 与被测代码之间的挂接点(fuzzer 从这里附着并注入测试用例),签名固定为:

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size);

构建与运行

Fuzzing 配置下构建 OpenConsole 解决方案,会输出一个直接运行 fuzzer 的可执行程序:对给定的测试用例文件执行注入。以仓库中的 conhost fuzzer 为例,期望的产物位于:

bin\x64\Fuzzing\OpenConsoleFuzzer.exe

该可执行文件由 src/host/ft_fuzzer/Host.FuzzWrapper.vcxproj 定义(TargetNameOpenConsoleFuzzer)。注意它在 Fuzzing 配置下才把 LibFuzzer 运行时加入链接行:

<ItemDefinitionGroup Condition="'$(Configuration)'=='Fuzzing'">
  <!-- 理论上我们可能希望在未启用 Fuzzing 时用普通 main() 构建,
       因此只在 Fuzzing 配置下把 fuzzer 加进链接行 -->
  <Link>
    <AdditionalDependencies>winmm.lib;imm32.lib;clang_rt.fuzzer_MT-$(OCClangArchitectureName).lib;%(AdditionalDependencies)</AdditionalDependencies>
  </Link>
</ItemDefinitionGroup>

clang_rt.fuzzer_MT-<arch>.lib 是 LibFuzzer 的主机运行时(MT = 多线程静态 CRT,与上面 MultiThreaded 设定一致),它提供 fuzzer 主循环:读取种子语料(seed corpus)→ 变异 → 调用 LLVMFuzzerTestOneInput → 依据覆盖反馈决定是否保留新用例。

conhost fuzzer 的实现剖析

fuzzmain.cpp 是这个 fuzzer 的全部逻辑,值得逐段理解:

  1. NullDeviceComm 设备桩。conhost 正常启动时会与 ConDrv 设备驱动通信。fuzz 环境里没有驱动,fuzzmain.cpp 定义了一个空实现的 IDeviceComm(第 14–56 行):ReadIo/ReadInput 中直接挂起当前线程,让 IO 线程安静退出——注释解释了原因:"fuzzer 不需要设备 IO 线程"。
  2. StartNullConsole(第 58–98 行)。在"连接"之前先把 globals.pDeviceComm 替换为 NullDeviceComm(注释直言 "Leak this"——fuzzer 进程生命周期内无需释放),再以 INVALID_HANDLE_VALUE 调用 ConsoleCreateIoThreadLegacy(注释指出该空句柄本会在 ConDrvDeviceComm 中被检出,而这里已被提前替换掉)。随后在锁内分配根进程句柄、伪造一份 CONSOLE_API_CONNECTINFO(80x25 缓冲区与窗口、标题 "Fuzzing Harness")并调用 ConsoleAllocateConsole 完成控制台分配,再初始化命令历史。
  3. 入口切换RunConhost() 是导出的宿主启动函数;而 fuzzer 入口通过 FUZZING_BUILD 宏切换(第 117–125 行):
#ifdef FUZZING_BUILD
extern "C" __declspec(dllexport) int LLVMFuzzerInitialize(int* /*argc*/, char*** /*argv*/)
#else
int main(int /*argc*/, char** /*argv*/)
#endif
{
    RETURN_IF_FAILED(RunConhost());
    return 0;
}

Fuzzing 构建下它成为 LibFuzzer 的初始化钩子(每个测试用例批次前执行一次),普通构建下则是 main,因此同一份代码两种用途。注释还提到一个有意思的取舍:传入 stdin/stdout 句柄本可以像 conpty 一样驱动它、顺带测试 VT 渲染器,但目前选择"像 conhost 一样驱动"。 4. 核心注入点(第 127–137 行):

extern "C" __declspec(dllexport) int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
    auto& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation();

    const auto u16String{ til::u8u16(std::string_view{ reinterpret_cast<const char*>(data), size }) };
    til::CoordType scrollY{};
    gci.LockConsole();
    auto u = wil::scope_exit([&]() { gci.UnlockConsole(); });
    WriteCharsLegacy(gci.GetActiveOutputBuffer(), u16String, &scrollY);
    return 0;
}

LibFuzzer 每次变异出的字节流 data 先经 til::u8u16 做 UTF-8 → UTF-16 转换,然后在持有控制台锁的情况下调用 WriteCharsLegacy 写入活动输出缓冲区。也就是说,模糊测试真正压测的是 conhost 的字符/控制序列写入路径:任何由恶意或畸形输入触发的越界写、非法状态、崩溃,都会在 ASAN 保护下以 sanitizer 报告的形式暴露。

仓库中的其他模糊测试形态

  • VT 解析器 fuzzer(定向模糊测试)src/terminal/parser/ft_fuzzer/VTCommandFuzzer.cpp 是一台基于令牌(token)生成的定向 fuzzer。它按 VT100 规格构造 ESC(0x1B)、CSI(ESC [,及 C1 单字节变体 0x9B)、OSC(ESC ])序列(第 13–23 行),并以概率表(g_tokenGenerators)随机组合 SGR、CUX、私有序列、设备属性查询、光标寻址、硬/软复位、VT52 序列等令牌,配合少量无效令牌与文本噪声。相比纯随机字节,这种"语法感知"的生成方式能更快到达解析器的深层状态,是模糊测试输入生成器设计的一个典型范例。
  • 小型函数级 fuzz 示例src/til/ut_til/string.cpp 中保留了一段 #if 0 包裹的 LLVMFuzzerTestOneInput(第 81–119 行),用于以 clang 的 strtoull 为参照对 parse_u64 做等价性模糊验证,注释记录了当时的运行方式:clang++ -fsanitize=address,undefined,fuzzer -std=c++17 file.cpp,16 个并行任务跑 20 分钟。它展示了不依赖完整解决方案也能给单个函数挂 fuzzer 的轻量路径;验证结果最终沉淀为该文件中正式的单元测试(如 parse_u64_overflow)。

使用 OneFuzz 在 CI 上运行 fuzzer

OneFuzz 允许把 fuzzer 跑在 CI 中,并在发现新 bug 时得到告警。以下流程继承自 doc/fuzzing.md

安装 OneFuzz CLI

从 OneFuzz 项目的 releases 页下载最新版 OneFuzz CLI(onefuzz 命令行工具)并安装到 PATH。

配置 OneFuzz

在本地运行 OneFuzz 前,需要配置 endpoint、client ID 与 client secret。Windows 团队有一份预设配置可参考(文档指向 osgwiki 上的 OneFuzz 配置教程页)。配置命令:

onefuzz config --endpoint $(endpoint) --client_id $(client_id) --authority $(authority) --tenant_domain $(tenant_domain)

注意:项目的流水线(pipeline)已经配置了这些变量,因此在 Azure DevOps 上运行时无需关心这一步。

在 OneFuzz 上运行任务

配置完成后,用如下命令创建一个 libfuzzer 任务:

onefuzz template libfuzzer basic <project> <name> <build> <pool> --target_exe <exe_path>

参数说明:

参数 含义
project 项目名
name 测试任务名称
build 构建标识(即 commit SHA1)
pool 运行该任务的 VM 池
exe_path 构建项目输出的 fuzzer 可执行文件路径(如 bin\x64\Fuzzing\OpenConsoleFuzzer.exe

命令执行后还会以 JSON 格式输出更多任务信息(例如 job ID),可用于后续查询与追踪。

启用通知

注意:项目流水线已内置该功能,此处仅是快速搭建指南,便于自行配置与调整。

OneFuzz 支持同时启用多种通知系统,包括 MS Teams 与 Azure DevOps(OneFuzz 的 getting-started、notifications 文档分别给出了 Teams 与 Azure DevOps 的配置方法)。本项目的流水线配置为在发现缺陷时自动创建 Azure DevOps 工作项,使每个 crash 都有可跟踪、可指派、可复现的最小用例(OneFuzz 会自动归档触发崩溃的输入)。

适用前提与限制

  • 本地构建需要 Windows 环境、Visual Studio(Fuzzing 配置使用 Clang 编译器选项与 clang_rt 运行时),仓库在 doc/building.md 中描述了总体构建前提;
  • Fuzzing 配置与普通 Debug/Release 构建依赖目录隔离(vcpkg-fuzzing),首次构建会额外拉取并按 triplet 重编第三方依赖,耗时明显更长;
  • conhost fuzzer 的 LLVMFuzzerTestOneInput 面向"以 conhost 方式驱动"的写入路径;注释中明确提到,若改用 conpty 方式驱动(传入 stdin/stdout),还能覆盖 VT 渲染器路径,这属于文档中标注的后续扩展方向而非当前默认行为;
  • 在 Azure DevOps 中直接运行时,endpoint/client 等变量由流水线提供,本地独立运行 OneFuzz 则必须自行完成 onefuzz config 步骤。

小结

OpenConsole 的模糊测试体系由两层构成:本地层是解决方案级 Fuzzing 配置(ASAN + 覆盖插桩 + 静态 CRT + LibFuzzer 运行时),产物如 Host.FuzzWrapper.vcxproj 输出的 OpenConsoleFuzzer.exe,通过 fuzzmain.cpp 中的 LLVMFuzzerTestOneInput 把变异字节流注入 conhost 的 WriteCharsLegacy 写入路径;CI 层是 OneFuzz,用 onefuzz template libfuzzer basic 把同一可执行文件挂到 VM 池上长期运行,并通过通知/工作项机制闭环缺陷。新增自己的 fuzzer 时,只需遵循同样的模式:提供 LLVMFuzzerTestOneInput 入口,让 Fuzzing 配置完成 sanitizer 与 fuzzer 运行时的装配。

登录后查看全文
热门项目推荐
相关项目推荐