首页
/ ripgrep 模糊测试实战:用 cargo-fuzz 守护 globset 模式转换的正确性

ripgrep 模糊测试实战:用 cargo-fuzz 守护 globset 模式转换的正确性

2026-09-03 21:41:55作者:董灵辛Dennis

本文以 ripgrep 仓库中的 fuzz/README.md 为主体,完整讲解 ripgrep 内置模糊测试(fuzz testing)组件的安装、目标列表查看、模糊测试运行与限时控制等完整操作流程,并结合 fuzz/Cargo.tomlfuzz/fuzz_targets/fuzz_glob.rsglobset 源码 剖析当前唯一模糊测试目标 fuzz_glob 的验证逻辑、arbitrary 特性背后的代码生成机制,以及模糊测试工程作为独立 workspace 的组织方式。读完后你可以独立运行、限时限地执行 ripgrep 的模糊测试,并理解它在类型系统之外捕获的稳定性问题究竟来自哪里。

一、模糊测试的定位:类型系统看不见的 bug

fuzz/README.md 的引言部分给出了本项目引入模糊测试的核心动机:

Fuzz testing produces pseudo-random / arbitrary data that is used to find stability issues within a code base. While Rust provides a strong type system, this does not guarantee that an object will convert properly from one struct to another. It is the responsibility of the developer to ensure that a struct is converted properly. Fuzz testing will generate input within the domain of each property. This arbitrary data can then be used to convert from ObjectA to ObjectB and then back. This type of testing will help catch bugs that the type system is not able to see.

也就是说,Rust 的强类型系统只能保证编译期类型安全,但无法保证对象在两个 struct 之间转换的正确性——例如同一个 glob 字符串,经由 Glob::new 构造与经由 FromStr::from_str 构造,两条路径是否得到完全一致的结果?类型检查器对此无能为力,这属于"转换正确性"的责任,只能靠开发者主动验证。模糊测试的做法是:针对每个属性在其取值域内生成任意(arbitrary)数据,用这些数据驱动 "ObjectA → ObjectB → ObjectA" 的往返转换,从而暴露类型系统看不见的 bug。ripgrep 仓库中当前落地的模糊测试目标正是围绕 globsetGlob 类型做这类往返验证,这与 ripgrep 作为行级正则搜索工具、底层依赖 glob 模式匹配路径过滤(crates/ignorecrates/globset)的定位直接相关。

二、工程组织:fuzz 目录是一个独立 workspace

模糊测试工程位于仓库根目录的 fuzz/ 下,其 fuzz/Cargo.toml 有几处关键设计:

[package]
name = "fuzz"
version = "0.0.1"
publish = false
edition = "2024"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
globset = { path = "../crates/globset", features = ["arbitrary"] }

# Prevent this from interfering with workspaces
[workspace]
members = ["."]

[profile.release]
debug = 1

[[bin]]
name = "fuzz_glob"
path = "fuzz_targets/fuzz_glob.rs"
test = false
doc = false

逐条解读:

  • publish = false:fuzz 包永远不会被发布到 crates.io,它是纯开发辅助工程。
  • [workspace] members = ["."]:显式声明自身为单成员 workspace。注释 "Prevent this from interfering with workspaces" 说明其意图——fuzz/ 位于主 workspace 的目录范围内,若不做此隔离,Cargo 会把它误认为主 workspace 的成员而引发冲突。相应地,主 Cargo.toml[workspace] members 列表中只列出了 crates/globsetcrates/grep 等 9 个 crate,并不包含 fuzz;主包的 exclude 列表中也明确排除了 fuzz 目录(条目写作 crates/fuzz),保证发布的 ripgrep 包不含模糊测试代码。
  • libfuzzer-sys = "0.4":libFuzzer 的 Rust 绑定,提供 libfuzzer_sys::fuzz_target! 宏与 #![no_main] 入口,是 cargo-fuzz 工作流的基础依赖。
  • globset = { path = "../crates/globset", features = ["arbitrary"] }:以路径依赖方式引入本地 globset crate,并启用其 arbitrary 特性——这是本仓库模糊测试能够"按属性域生成数据"的关键(详见第六节)。
  • [profile.release] debug = 1:模糊测试默认在 release 档编译(优化后的代码路径更能暴露真实运行状态下的问题),但保留 1 级调试信息,便于崩溃时生成可读的栈回溯与报告。
  • [[bin]]:显式注册名为 fuzz_glob、路径为 fuzz_targets/fuzz_glob.rs 的可执行目标,且 test = falsedoc = false——模糊目标不是常规二进制,不需要 cargo test 和文档生成。

三、安装:cargo install cargo-fuzz

fuzz/README.md 说明本 crate 依赖 cargo-fuzz 组件,安装方式是在 fuzz 目录下执行:

cargo install cargo-fuzz

cargo-fuzz 会为 fuzz/ 目录注入一套基于 libFuzzer 的构建与运行工作流(自动维护一个临时的 fuzz workspace 并链接 libfuzzer-sys 生成的目标),因此后续所有 cargo fuzz ... 子命令都应在 fuzz/ 目录下执行。

四、列出模糊测试目标

安装完成后,执行:

cargo fuzz list

该命令会打印出所有可测试的模糊目标列表。对当前仓库而言,依据 fuzz/Cargo.toml[[bin]] 的定义,列表里会出现 fuzz_glob 这一个目标——它对应 fuzz/fuzz_targets/fuzz_glob.rs 文件。

五、运行模糊测试与限时控制

运行模糊测试必须显式指定目标:

cargo fuzz run <target>

需要注意:以上命令会无限期运行。README 指出,应使用 -max_total_time=<num seconds> 标志指定测试运行时长(秒):

cargo fuzz run <target> -- -max_total_time=5

上述命令让模糊测试运行 5 秒(本仓库的实际写法即 cargo fuzz run fuzz_glob -- -max_total_time=5-- 之后的参数直接透传给 libFuzzer)。README 还描述了两种结果行为:

  • 正常完成:若测试在时限内没有触发错误,会输出成功执行的用例数量(executed N corpus entries / N runs/sec 一类统计)。
  • 失败中止:一旦产生错误,测试会以非零错误码中止,并打印触发问题的 arbitrary 输入——这正是模糊测试的核心产出:一个可直接复现缺陷的最小语料,可据此写回归用例。

六、深入目标源码:fuzz_glob 验证什么

fuzz/fuzz_targets/fuzz_glob.rs 全文如下:

#![no_main]

use std::str::FromStr;

use globset::Glob;

libfuzzer_sys::fuzz_target!(|glob_str: &str| {
    let Ok(glob) = Glob::new(glob_str) else {
        return;
    };

    let Ok(glob2) = Glob::from_str(glob_str) else {
        return;
    };

    // Verify that a `Glob` constructed with `new` is the same as a `Glob`` constructed
    // with `from_str`.
    assert_eq!(glob, glob2);

    // Verify that `Glob::glob` produces the same string as the original.
    assert_eq!(glob.glob(), glob_str);
});

这段代码恰好是 README 引言中 "ObjectA → ObjectB → 再转回来" 思路的具体实现,包含两个不变量断言:

  1. 双构造路径等价Glob::new(glob_str)Glob::from_str(glob_str) 的构造结果必须 assert_eq! 相等。从 crates/globset/src/glob.rs 源码看,Glob::new(约 L283-L285)实现为 GlobBuilder::new(glob).build();而 FromStr for Glob(约 L123-L129)的 from_str 直接委托 Self::new(glob)。二者当前实现上是同一条路径,该断言的价值在于防止未来某条路径被独立修改后悄然偏离——即守护 "两个公开 API 保持行为一致" 这一契约。
  2. round-trip 保真:构造成功时,glob.glob() 返回的原始 glob 字符串必须与输入 glob_str 完全一致。对照源码,Glob 结构体内保存了 glob: String 字段,pub fn glob(&self) -> &str(约 L308-L310)原样返回它,因此该断言验证的是"编译 glob 不丢失、不改写原始模式文本"。

两处 let Ok(...) else { return; }; 表明:解析失败(返回 Err)是合法的模糊输入结果,直接跳过、不做断言——模糊器会大量生成非法 glob(如不闭合的括号、孤立 \ 等),只要解析过程不 panic、不越界,就视为通过。

七、arbitrary 特性:数据如何按属性域生成

模糊目标签名为 |glob_str: &str|,libFuzzer 通过 libfuzzer-sys 对字符串做字节级变异。而 fuzz/Cargo.tomlglobset = { path = "../crates/globset", features = ["arbitrary"] } 启用的是 globset 的结构化随机能力。在 crates/globset/Cargo.toml 中可以看到特性定义:

arbitrary = { version = "1.3.2", optional = true, features = ["derive"] }
...
[features]
default = ["log"]
arbitrary = ["dep:arbitrary"]

arbitrary 特性默认关闭,仅当显式启用时才引入 arbitrary crate 的依赖。启用后,crates/globset/src/glob.rs 中多个核心类型会被条件性地派生 Arbitrary trait:

#[derive(Clone, Eq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Glob { ... }

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
struct GlobOptions { ... }

#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
struct Tokens(Vec<Token>);

#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
enum Token { Literal(char), Any, ZeroOrMore, ... }

这意味着模糊器可以直接在 GlobGlobOptions、token 序列等结构的属性域内生成任意数据(每个字段取合法值域内的随机值),而不是只有裸字符串这一种入口。这正是 README 所说 "Fuzz testing will generate input within the domain of each property" 的源码级落地。crates/globset/src/lib.rs 的 crate 级文档同样列出了该特性:启用后 Glob 类型会实现 arbitrary crate 的 Arbitrary trait,且默认禁用——普通下游使用者不受影响。

八、实操清单与适用前提

把上述信息收敛为可直接执行的步骤(均基于当前仓库状态,ripgrep 版本 15.2.0,见 Cargo.toml):

  1. 进入 fuzz/ 目录(所有 cargo fuzz 子命令在此执行);
  2. cargo install cargo-fuzz 安装组件;
  3. cargo fuzz list 查看目标,确认存在 fuzz_glob
  4. cargo fuzz run fuzz_glob -- -max_total_time=5 限时运行 5 秒,观察输出;
  5. 若测试以非零码中止,抓取打印出的 arbitrary 输入作为复现语料。

适用前提与限制:

  • 模糊测试目标按 release 档编译并依赖 libFuzzer 的 sanitizer 支持,需具备可用的 Rust 工具链(通常需 nightly 组件支持),fuzz/Cargo.toml 声明 edition = "2024"、主 workspace 声明 rust-version = "1.96",应使用足够新的工具链;
  • fuzz/ 是独立 workspace 且 publish = false不参与 cargo build/cargo test 在主 workspace 下的默认构建,也不随发布的 ripgrep 包分发;
  • 当前仓库只有一个模糊目标 fuzz_glob,其保护面是 globset 的 glob 解析/构造一致性;其余 crate(searcher、ignore 等)在现有仓库状态中未见对应的模糊目标。

九、小结

ripgrep 的 fuzz/ 目录以最小的工程代价(一个独立单成员 workspace、一个 libfuzzer-sys 目标)建立了对 globset 关键 API 的持续性稳定性防线:cargo fuzz list 枚举目标、cargo fuzz run <target> -- -max_total_time=N 限时驱动,失败时输出可复现的 arbitrary 输入。而 globset 侧的 arbitrary 特性(条件派生 Arbitrary)则让随机数据生成深入到结构属性域内部,两者配合,正好覆盖了强类型系统保证不了的"转换正确性"这一类缺陷。

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