OpenConsole 模糊测试指南:用 Fuzzing 构建配置与 OneFuzz 对 Windows 控制台宿主做 LibFuzzer 模糊测试
【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal
本篇基于 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|Win32、Fuzzing|x64、Fuzzing|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.lib、clang_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-statictriplet 基础上做了两点强化:
# 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.vcxproj 将ConfigurationType从 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 定义(TargetName即OpenConsoleFuzzer)。注意它在 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 的全部逻辑,值得逐段理解:
- NullDeviceComm 设备桩。conhost 正常启动时会与 ConDrv 设备驱动通信。fuzz 环境里没有驱动,fuzzmain.cpp 定义了一个空实现的
IDeviceComm(第 14–56 行):ReadIo/ReadInput中直接挂起当前线程,让 IO 线程安静退出——注释解释了原因:"fuzzer 不需要设备 IO 线程"。 - StartNullConsole(第 58–98 行)。在"连接"之前先把
globals.pDeviceComm替换为NullDeviceComm(注释直言 "Leak this"——fuzzer 进程生命周期内无需释放),再以INVALID_HANDLE_VALUE调用ConsoleCreateIoThreadLegacy(注释指出该空句柄本会在ConDrvDeviceComm中被检出,而这里已被提前替换掉)。随后在锁内分配根进程句柄、伪造一份CONSOLE_API_CONNECTINFO(80x25 缓冲区与窗口、标题 "Fuzzing Harness")并调用ConsoleAllocateConsole完成控制台分配,再初始化命令历史。 - 入口切换。
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 运行时的装配。
【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考