PowerToys AdvancedPaste 模糊测试工程实战:使用 OneFuzz 对 .NET 代码开展 Fuzz 测试
本指南以 src/modules/AdvancedPaste/AdvancedPaste.FuzzTests/Fuzz.md 为核心,系统讲解微软 PowerToys 仓库中 AdvancedPaste 模块如何依托微软内部 OneFuzz 服务对其 .NET 代码(特别是剪贴板内容转 JSON 的 JsonHelper)进行持续模糊测试:包括模糊测试工程的组织方式、被测代码的链接策略、fuzz 入口函数的写法、OneFuzzConfig.json 作业配置逐字段解析,以及从本地运行到云端作业分发的完整流程。阅读本文后,你既能理解该工程"为何如此搭建",也能掌握在类似 .NET 项目中复刻一套 OneFuzz 模糊测试流水线的可操作方法。
一、工程背景:PowerToys 中为什么需要一个 .NET Fuzz 工程
PowerToys 的 AdvancedPaste(高级粘贴)是一个对剪贴板内容执行"转为纯文本 / Markdown / JSON / 自定义格式"等变换的 Windows 生产力工具,其核心逻辑位于 src/modules/AdvancedPaste/AdvancedPaste 下的托管代码。这类"吃任意文本输入、产出结构化数据"的解析器(尤其是 XML、INI、CSV 这类需要正则与引号转义处理的文本格式解析)天然适合用模糊测试来发现崩溃与未处理异常。
仓库因此在模块旁独立维护了一个专门的模糊测试工程 src/modules/AdvancedPaste/AdvancedPaste.FuzzTests,目录下四个文件构成完整闭环:
| 文件 | 职责 |
|---|---|
| FuzzTests.cs | fuzz 入口函数,OneFuzz 反复调用它并投喂随机字节 |
| AdvancedPaste.FuzzTests.csproj | 工程定义:把被测代码"链接"进工程而非复制 |
| OneFuzzConfig.json | 云端作业部署描述(libfuzzer .NET 目标、bug 上报、依赖清单) |
| Logger.cs | 规避外部依赖的空实现日志占位 |
该工程已被纳入主解决方案 PowerToys.slnx(第 156 行声明 AdvancedPaste.FuzzTests/AdvancedPaste.FuzzTests.csproj),说明它不是一次性脚本,而是随 CI 一起构建、为 OneFuzz 持续摄取服务的常态化工程。
二、为什么用 .NET 8(以及仓库当前的演进状态)
原文档 Fuzz.md 的 "Why Use .NET 8 (Windows)" 一节给出两条理由,属于撰写该文档时点的官方现状说明:
- 当前支持范围:撰写时 OneFuzz 仅支持 .NET 8 工程,Fuzz 团队正在推进 .NET 9 支持;
- 过渡方案:在 .NET 9 支持落地前,.NET 8 是稳定且临时的选择,且支持"直接链接代码文件",便于高效开发。
对照仓库当前源码可以发现,这一约束已随上游演进被更新。src/Common.Dotnet.FuzzTest.props 的文件头注释完整记录了这段历史并给出了现状:
Fuzz test projects pin their target framework here so it can be managed independently of the main product TFM... This was historically .NET 8 because OneFuzz did not support newer runtimes. Per the current OneFuzz .NET fuzzing docs the service is runtime-agnostic (".NET Core targets are preferred") and keys off the build drop directory, so the fuzz projects now track net10 like the rest of the repo.
即:该 props 将模糊测试工程的 TFM 与主产品 TFM 解耦单独管理;历史上锁定为 .NET 8(OneFuzz 不支持更新的运行时);而按当前 OneFuzz .NET 模糊测试文档,服务已运行时无关,因此 AdvancedPaste.FuzzTests.csproj 这类工程现在跟随全仓库统一的目标框架 net10.0-windows10.0.26100.0(见 src/Common.Dotnet.FuzzTest.props 第 11 行)。
需要特别说明一个仓库内的"时间差":被测文件 FuzzTests.cs 顶部仍保留着 ".NET 8 / 临时方案" 时代的注释("OneFuzz currently does not support .NET 9 code testing, so this is a temporary solution. Create a .NET 8 project and use a file link to include the code for testing first."),这与 props 中已迁移到 net10 的现状并存。阅读时建议以 src/Common.Dotnet.FuzzTest.props 的说明为准:".NET 8 临时方案"是历史动机,工程当前实际编译目标已与仓库一致为 net10。无论 TFM 如何变化,"把被测试代码文件直接链接进 fuzz 工程"这一组织方式始终未变。
三、工程结构剖析:链接(Link)而非复制被测代码
AdvancedPaste.FuzzTests.csproj 的精髓在链接机制。其工程文件核心片段如下(src/modules/AdvancedPaste/AdvancedPaste.FuzzTests/AdvancedPaste.FuzzTests.csproj):
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<Import Project="$(RepoRoot)src\Common.Dotnet.FuzzTest.props" />
<PropertyGroup>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<TestingPlatformCommandLineArguments>$(TestingPlatformCommandLineArguments) --ignore-exit-code 8</TestingPlatformCommandLineArguments>
</PropertyGroup>
<PropertyGroup>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\tests\AdvancedPaste.FuzzTests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\AdvancedPaste\Helpers\JsonHelper.cs" Link="JsonHelper.cs" />
</ItemGroup>
...
<ItemGroup>
<Content Include="OneFuzzConfig.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
要点逐条展开:
- 链接被测文件:
<Compile Include="..\AdvancedPaste\Helpers\JsonHelper.cs" Link="JsonHelper.cs" />不复制代码,而是把主工程里的 src/modules/AdvancedPaste/AdvancedPaste/Helpers/JsonHelper.cs 以JsonHelper.cs的逻辑名编入本工程编译。这样 fuzz 永远针对最新真实实现,不会有"拷贝一份改坏了没人发现"的漂移问题——这正是原文档所称"direct code linking for efficient development"。 - 独立于主产品 TFM:通过导入 src/Common.Dotnet.FuzzTest.props 单独钉住框架版本,避免与产品线(
Common.Dotnet.CsWinRT.props)互相牵制。 - 引入 WinRT 投影:导入
Common.Dotnet.CsWinRT.props后获得Microsoft.Windows.CsWinRT支持,使 fuzz 代码能引用Windows.ApplicationModel.DataTransfer(DataPackage/DataPackageView),这是被测函数签名所依赖的 WinRT 类型。 - MSTest 宿主但无测试用例:引用
MSTest包仅为获得可执行宿主;本工程没有任何[TestMethod],因此会触发"无测试运行"的退出码 8,工程用--ignore-exit-code 8显式放行(文件内注释 "exit code 8 means no tests ran" 即说明这一点)。 - 与 CI 集成:
Common.Dotnet.FuzzTest.props中在TF_BUILD(Azure Pipelines 环境变量)非空时设置TestingPlatformDisableCustomTestTarget=true,避免 CI 的Build;Test全量阶段把这个 fuzz 工程当普通单测跑——它只为 OneFuzz 摄取而构建,不在常规测试阶段执行。 - 配置随输出走:
OneFuzzConfig.json以PreserveNewest复制到输出目录,确保产物目录里总是带上最新作业描述。 - 无外部依赖的 Logger 桩:Logger.cs 刻意放在
ManagedCommon命名空间内、提供全空实现的LogTrace/LogInfo/LogWarning/LogError/LogDebug方法。原因是JsonHelper源码using ManagedCommon并调用Logger.LogTrace()等(见 JsonHelper.cs 第 52、56 行等),fuzz 工程不希望为满足这一引用去链接整套日志基础设施,于是用同名空实现占位(其注释:"This is used for fuzz testing and ensures that the project links only to JsonHelper, avoiding unnecessary connections to additional files")。这是一种非常实用的最小依赖链接技巧。
四、fuzz 入口函数实现剖析:FuzzToJsonFromXmlOrCsv
OneFuzz 的 .NET fuzzer(libfuzzerDotNet 类型)要求被测工程暴露一个接收 ReadOnlySpan<byte> 的静态方法作为变异入口。本工程的入口在 src/modules/AdvancedPaste/AdvancedPaste.FuzzTests/FuzzTests.cs:
public static void FuzzToJsonFromXmlOrCsv(ReadOnlySpan<byte> input)
{
string text = Encoding.UTF8.GetString(input);
var dataPackage = new DataPackage();
dataPackage.SetText(text);
try
{
_ = Task.Run(async () => await JsonHelper.ToJsonFromXmlOrCsvAsync(dataPackage.GetView()))
.GetAwaiter().GetResult();
}
catch (Exception ex) when (ex is ArgumentException)
{
throw;
}
}
代码里藏着三个值得学习的工程细节:
1. 显式 UTF-8 解码而非字节直通。 代码注释明确指出:对 ReadOnlySpan<byte> 直接调用 ToString() 只会得到类型名(如 "System.ReadOnlySpan<Byte>[N]"),而不是实际字节内容,因此必须先用 Encoding.UTF8.GetString(input) 把 OneFuzz 产生的随机字节解码成真正会被解析的文本,否则 fuzz 就形同空转。
2. 用 GetAwaiter().GetResult() 而非 Task.Run(...).Result。 被测的 ToJsonFromXmlOrCsvAsync 是 async Task<string>,在 fuzz 入口里同步等待时若用 Task.Result,抛出的异常会被包装成 AggregateException,导致下方 when (ex is ArgumentException) 异常过滤器永远无法命中;GetAwaiter().GetResult() 则会原样上抛原始异常类型,从而让过滤器精确工作。
3. 异常过滤器的正确语义。 catch (Exception ex) when (ex is ArgumentException) { throw; } 是一段"声明式"代码:它不吞掉任何异常,只是借助 catch 子句的过滤语义明确表达意图——ArgumentException(如 INI 空节名、CSV 非法分隔符导致的 FormatException 之外的参数错误)是允许出现的预期路径,需要放行以便 OneFuzz 记录为"良性样本";而注释强调"捕获所有异常是反模式,可能掩盖 NullReferenceException 这类代码自身缺陷",因此最终一律 throw 重新抛出。由于 ToJsonFromXmlOrCsvAsync 的方法契约本就不应抛异常,重新抛出等于把任何真实缺陷都如实暴露给 fuzzer 统计与上报。
五、被测对象纵深:JsonHelper.ToJsonFromXmlOrCsvAsync 为什么值得被 Fuzz
被链接进来的 src/modules/AdvancedPaste/AdvancedPaste/Helpers/JsonHelper.cs 是 AdvancedPaste "粘贴为 JSON" 功能背后的格式转换器。ToJsonFromXmlOrCsvAsync(DataPackageView clipboardData) 的识别链依次为(可对照源码第 50–229 行):
- 读取剪贴板文本:先检查
clipboardData.Contains(StandardDataFormats.Text),再GetTextAsync();读取失败(WinRT/COM 层异常,如剪贴板被占用、其他应用注入畸形负载)按契约返回string.Empty,绝不外抛; - 已是 JSON 则原样返回:
IsJson用System.Text.Json的JsonDocument.Parse试探性解析; - XML → JSON:
XmlDocument.LoadXml后经Newtonsoft.Json的JsonConvert.SerializeXmlNode转换; - INI → JSON:要求首行为节名
[...]、次行为节名或键=值(正则见IniSectionNameRegex/IniValueLineRegex),跳过;注释行,非法行抛FormatException(如"Empty section name"); - CSV → JSON:先按
sep=X指令或,、;、\t候选集做分隔符探测(GetCsvDelimiter,以首两行的分隔符出现次数一致性防误判),再对每行做引号配对校验与分隔符切分,最后用ReplaceQuotationMarksInCsvData三连正则清理包裹/转义引号; - 纯文本兜底:整段按行拆分后序列化为 JSON 字符串数组。
这段代码包含大量手写正则、边界条件与"尝试-捕获-降级"链路(XML 失败降级 INI、再降级 CSV、再降级纯文本),任何一步的正则回溯、索引越界、引号配对误判都可能制造崩溃或死循环——这正是模糊测试的理想靶标,也是该 FuzzTests 工程选择其作为 FuzzToJsonFromXmlOrCsv 被测方法的原因。可以推断:通过随机字节持续轰炸该入口,相当于自动化验证了这条格式识别链在畸形输入下的健壮性。
六、OneFuzzConfig.json:云端作业配置逐字段拆解
OneFuzzConfig.json 是 OIP(OneFuzz Ingestion Preparation)工具与摄取服务部署 fuzz 作业时读取的核心描述文件。原文档强调其整体结构为一个配置条目数组,数组之外仅保留 configVersion 字段用于追踪配置 schema 的变更版本。仓库中 src/modules/AdvancedPaste/AdvancedPaste.FuzzTests/OneFuzzConfig.json 是一份可参照的完整 V3 实例,全文如下:
{
"configVersion": 3,
"entries": [
{
"fuzzer": {
"$type": "libfuzzerDotNet",
"dll": "AdvancedPaste.FuzzTests.dll",
"class": "AdvancedPaste.FuzzTests.FuzzTests",
"method": "FuzzToJsonFromXmlOrCsv",
"FuzzingTargetBinaries": [
"PowerToys.AdvancedPaste.dll"
]
},
"adoTemplate": {
"org": "microsoft",
"project": "OS",
"AssignedTo": "leilzh@microsoft.com",
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
"IterationPath": "OS\\Future"
},
"jobNotificationEmail": "PowerToys@microsoft.com",
"skip": false,
"rebootAfterSetup": false,
"oneFuzzJobs": [
{
"projectName": "AdvancedPaste",
"targetName": "AdvancedPaste-dotnet-fuzzer"
}
],
"jobDependencies": [
"AdvancedPaste.FuzzTests.dll",
"AdvancedPaste.FuzzTests.pdb",
"Microsoft.Windows.SDK.NET.dll",
"Newtonsoft.Json.dll",
"WinRT.Runtime.dll"
],
"SdlWorkItemId": 49911822
}
]
}
对照官方 V3 schema 与仓库实际使用,各字段语义如下:
fuzzer:声明 fuzzer 类型与 .NET 目标。$type: libfuzzerDotNet:选择 .NET 托管 fuzzer;dll:承载 fuzz 入口的程序集,即本工程产物AdvancedPaste.FuzzTests.dll;class/method:fuzz 入口的完全限定类型与方法名,必须与 FuzzTests.cs 中AdvancedPaste.FuzzTests.FuzzTests.FuzzToJsonFromXmlOrCsv一致;FuzzingTargetBinaries:声明被测的主产品二进制PowerToys.AdvancedPaste.dll,便于 OneFuzz 关联符号与覆盖率。
adoTemplate:指定缺陷上报到哪个 Azure DevOps 工作项模板——仓库用microsoft/OS项目,并预设AssignedTo、AreaPath、IterationPath(注释明确提示:应按项目自身填值,bug 将据此自动归档)。配置里属于微软内部路径,其他组织复用时需整体替换。jobNotificationEmail:作业通知邮箱,仓库中为模块团队别名PowerToys@microsoft.com。skip:是否跳过该条目的作业调度(false表示正常参与摄取)。rebootAfterSetup:虚拟机在完成环境准备后是否需要重启再开跑。oneFuzzJobs:至少一项,定义 OneFuzz 侧的作业归属,此处为AdvancedPaste项目下的AdvancedPaste-dotnet-fuzzer目标。jobDependencies:摄入到 fuzz 节点的文件清单,注释要求至少包含 DLL 与 PDB,且支持 glob。此例中的五项可逐一对上依赖链:AdvancedPaste.FuzzTests.dll/.pdb:fuzz 宿主及其符号;Microsoft.Windows.SDK.NET.dll+WinRT.Runtime.dll:WinRT/CsWinRT 投影运行时,因为入口与JsonHelper都触碰DataPackage/DataPackageView;Newtonsoft.Json.dll:被测JsonHelper中JsonConvert.SerializeXmlNode/SerializeObject的实际依赖。
SdlWorkItemId:关联的 SDL(安全开发生命周期)工作项编号,用于追踪该 fuzz 目标的安全审核记录。
由此也可以反向印证 src/modules/AdvancedPaste/AdvancedPaste.FuzzTests/AdvancedPaste.FuzzTests.csproj 中 OutputPath 刻意指向 tests\AdvancedPaste.FuzzTests\ 输出目录的用意——便于摄取流水线把上述 dll/pdb 连同 OneFuzzConfig.json(PreserveNewest 复制)作为一个自洽的产物集打包下发。
七、从本地验证到云端作业的完整流程
原文档把端到端流程组织为四步,虽其具体的登录页、操作手册均为微软内部 OneFuzz 服务链接,但流程骨架与仓库现状完全自洽,可按下述方式落地:
1. 申请访问(Requesting Access)。 OneFuzz 生产实例的 CLI 登录受权限管控:文档明确要求先通过微软内部 MyAccess 访问包申请页完成审批,获批后才能获得 CLI 登录凭据。此步对应原文档 "Requesting Access" 一节;对 PowerToys 这类微软仓库,访问主体是具备内部账号的维护者。
2. 配置并构建 fuzz 工程(前提条件)。 在仓库根目录下把 AdvancedPaste.FuzzTests 作为解决方案成员编译(它已登记在 PowerToys.slnx),得到带 OneFuzzConfig.json 的输出目录;若本机同样安装了 .NET SDK 与 MSTest 运行环境,理论上可用 dotnet run --project src/modules/AdvancedPaste/AdvancedPaste.FuzzTests 之类方式验证入口能被正常加载(工程无测试用例,需接受退出码 8 的放行逻辑)。
3. 本地运行 .NET fuzz 目标(Running a .NET Fuzz Target Locally)。 原文档强调:在本地验证一个 .NET fuzz 目标需要特定配置,不能想当然地直接执行。通常意味着要用与 OneFuzz 相同的 libfuzzer 驱动宿主加载 AdvancedPaste.FuzzTests.dll 并以少量种子(seed corpus)跑一轮冒烟,确认入口方法无启动期崩溃——对应文档指向的官方手册中该专项小节。
4. 用 OIP 工具 + OneFuzz CLI 分发云端作业。 文档 "Tools" 一节明确了两类工具的分工:
- OIP(OneFuzz Ingestion Preparation)工具:负责读取
OneFuzzConfig.json、打包jobDependencies产物并准备摄取数据; - OneFuzz CLI:提供管理、执行作业的命令(创建作业、查询状态、下载崩溃复现样本等),需按其指引下载与初始化登录。
最后依据 oneFuzzJobs 中声明的项目/目标名把作业注册进 OneFuzz,由云端在装有 Windows 的 fuzz VM 上运行 libfuzzerDotNet,持续对 FuzzToJsonFromXmlOrCsv 投喂变异输入;一旦触发未被异常过滤器接受的缺陷(如 NullReferenceException),OneFuzz 将按 adoTemplate 在 Azure DevOps 中登记 bug,并按 jobNotificationEmail 通知维护团队——这就构成了"代码提交 → 构建 → 摄取 → 云端持续 fuzz → 缺陷自动回流"的闭环。
八、小结:可复用的工程范式
纵观 AdvancedPaste.FuzzTests 目录与 src/Common.Dotnet.FuzzTest.props,PowerToys 在 AdvancedPaste 上的做法可提炼为四条可移植经验:
- 共享 TFM 管理:把 fuzz 工程的框架版本通过公共 props 与产品 TFM 解耦,历史上随 OneFuzz 的 .NET 8 限制锁定、当前跟随仓库升级到 net10,规避上游能力漂移对主工程的冲击;
- 链接而非拷贝:用 MSBuild
Compile/Link把被测源文件(此处为JsonHelper.cs)直接编入 fuzz 工程,配合ManagedCommon.Logger空实现桩切断无关依赖,保证"fuzz 的就是产品里真实在跑的那份代码"; - 最小可测入口 + 精确异常语义:只暴露
void Fuzz(ReadOnlySpan<byte>)静态入口,显式 UTF-8 解码、GetAwaiter().GetResult()保真异常类型、用when过滤器只放行预期异常并一律重新抛出,把良性输入与真实缺陷区分交给 OneFuzz 统计; - 作业描述与产物同源:
OneFuzzConfig.json(configVersion+entries[])声明 fuzzer 类型、上报模板、作业归属与依赖清单,并随构建输出目录同步发布,使 OIP 摄取与云端调度无需额外人工编排。
对希望在自有 .NET 工程(尤其是含手写文本解析器、正则密集型代码的模块)中引入持续模糊测试的团队,本仓库这份工程即是一份开箱可对照的蓝本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00