首页
/ Nushell 中 nu-parser 的模糊测试实践:用 cargo-fuzz 为解析器做崩溃防护

Nushell 中 nu-parser 的模糊测试实践:用 cargo-fuzz 为解析器做崩溃防护

2026-09-05 10:12:22作者:咎竹峻Karen

本篇指南基于 Nushell 仓库中 crates/nu-parser/fuzz/README.md 展开,讲解如何为 Nushell 的语法解析器 nu-parser 搭建并运行基于 cargo-fuzz 的模糊测试(Fuzzing)环境。读完本篇,你将能够:独立准备初始种子语料(seed corpus)、正确启动两个 fuzz 目标(parseparse_with_keywords)、理解两个目标在代码路径覆盖上的差异,并了解 fuzz 目标如何直接调用 nu-parser 的核心入口函数,从而让任意字节流输入安全地穿过 Nushell 的词法分析与语法解析逻辑。

为什么要给 nu-parser 做模糊测试

nu-parser 是 Nushell 的语法解析核心:任何 Nushell 脚本在求值之前都要先经过它的词法分析(lexing)与语法分析(parsing),把字节流转换成 Block 形式的 AST。解析器是典型的高价值 fuzz 目标——它直接消费不可信的用户输入(脚本文件、管道内容、粘贴的表达式),任何未处理的边界情况(畸形引号、不匹配的括号、非法转义、编码异常等)都可能触发越界、死循环或 panic。

Nushell 为此在 crates/nu-parser/fuzz/ 下维护了一个标准的 cargo-fuzz 工程,包含两个 fuzz 目标、一个种子语料收集脚本,以及独立于主 workspace 的构建配置。整个目录结构如下:

crates/nu-parser/fuzz/
├── Cargo.toml                 # fuzz 工程配置(独立 workspace)
├── README.md                  # 使用指南
├── gather_seeds.nu            # 用 Nushell 脚本收集种子语料
├── rust-toolchain.toml        # 锁定 nightly 工具链
└── fuzz_targets/
    ├── parse.rs               # 目标 1:空引擎状态下仅做词法/语法分析
    └── parse_with_keywords.rs # 目标 2:加载核心关键字命令后解析

快速上手:四步启动 Fuzzer

按照 README 的 Quick start guide,完整流程只需四步:

1. 安装 cargo-fuzz

cargo install cargo-fuzz

cargo-fuzz 是 Rust 官方推荐的模糊测试框架封装(详见其官方项目文档),它会自动生成基于 libFuzzer 的目标二进制。本工程 Cargo.toml 中声明了对应的依赖:

[dependencies]
libfuzzer-sys = "0.4"

注意 rust-toolchain.toml 将工具链锁定为 nightly——libFuzzer 的 Rust 集成(#![no_main] + fuzz_target! 宏、-Z 相关 sanitizer 能力)需要 nightly 编译器支持,所以在该目录下构建时会自动切换工具链。

2. 运行 gather_seeds.nu 收集种子语料

nu crates/nu-parser/fuzz/gather_seeds.nu

种子语料(seed corpus)决定 fuzzer 的"起点多样性":起点越接近真实输入,变异搜索越高效。gather_seeds.nu 的完整逻辑只有几行,且完全用 Nushell 语法写成了"用 Nushell 测试 Nushell":

# Check if 'seeds' directory exists. If not, create one.
let seeds_exists = "./seeds" | path exists
if $seeds_exists == false { mkdir seeds }

# Gather all "*.nu" files from '../..' and copy them into 'seeds'
ls ../../**/*.nu | get name | each {|f| cp $f ./seeds/}

它做的事是:确保 seeds/ 目录存在,然后把当前仓库检出内容中所有 *.nu 文件(../../ 即仓库根目录起的递归 glob)复制到 seeds/ 中。这意味着种子语料天然包含了 Nushell 标准库、测试 fixtures、配置模板等真实脚本——README 同时提示,你还可以手动往 seeds/ 里追加更多文件来进一步增加输入多样性。

3. 创建输出目录

mkdir out

out/ 是 fuzz 运行产物的默认落点:崩溃输入(crash)、超时样本(timeout)以及自动精简后的语料都会写到这里。

4. 启动 fuzzer

cargo fuzz run parse out seeds

命令的三段结构是:cargo fuzz run <target> <artifact-dir> <corpus-dir>,其中 parse 是目标名(见下文),out 指定产物目录,seeds 指定初始语料目录。要运行第二个目标,只需替换目标名:

cargo fuzz run parse_with_keywords out seeds

两个 Fuzz 目标:覆盖范围的取舍

README 的 "Targets" 章节定义了本工程的核心设计决策:用两个目标覆盖不同深度的解析路径。两个目标的源码都非常短,差异恰恰体现在它们如何构造 EngineState 上。

目标 1:parse——纯粹的词法与语法分析

fuzz_targets/parse.rs 全文:

#![no_main]

use libfuzzer_sys::fuzz_target;

use nu_parser::*;
use nu_protocol::engine::{EngineState, StateWorkingSet};

fuzz_target!(|data: &[u8]| {
    let engine_state = EngineState::new();
    let mut working_set = StateWorkingSet::new(&engine_state);

    let _block = parse(&mut working_set, None, data, true);
});

关键点:

  • EngineState::new() 构造的是一个空引擎状态——不注册任何命令。README 对此的定位是:该目标"只引入 nu-parser,触达词法和解析逻辑,不执行任何命令"。
  • 每次 fuzz 迭代直接把随机字节流 data: &[u8] 交给 parse()。注意这里传入的是任意字节而非合法 UTF-8 文本,fuzz 器会主动构造各种非法编码、截断字符串等输入,这正是词法分析阶段最容易被打挂的地方。
  • 解析产物 _block 被显式丢弃——fuzz 阶段只做解析,不求值,因此不存在副作用风险。

目标 2:parse_with_keywords——让解析器"看见"核心关键字

fuzz_targets/parse_with_keywords.rs 全文:

#![no_main]

use libfuzzer_sys::fuzz_target;

use nu_cmd_lang::create_default_context;
use nu_parser::*;
use nu_protocol::engine::StateWorkingSet;

fuzz_target!(|data: &[u8]| {
    let engine_state = create_default_context();
    let mut working_set = StateWorkingSet::new(&engine_state);

    let _block = parse(&mut working_set, None, data, true);
});

与目标 1 唯一的实质区别在于 EngineState 的构造方式:这里调用的是 nu-cmd-lang 提供的 create_default_context(),它会注册 Nushell 核心关键字命令(defletiffor 等语言内建命令)的声明。

为什么要多注册这些声明?从解析器的实现结构看,nu-parser 在遇到 ifdefmatch 这类关键字时,会走与"调用一个普通命令"完全不同的专用解析分支——这些分支是否可达、以及如何解析,取决于当前 working set 中是否存在对应声明。因此 README 指出,该目标"让 fuzzer 触达更多代码路径,因为部分逻辑依赖这些声明的可用性",并且"可能执行到关键字命令的 const eval(常量求值)代码路径"。README 同时给出了当前边界声明:截至当前版本,该命令集在 const eval 阶段不应产生负面副作用,且该目标整体不会执行代码——这是一个对 fuzz 安全性的明确保证,也是该目标敢长期挂在仓库里的原因。

两条路径的最终汇聚点:parse()

无论哪个目标,最终都汇入 nu-parser 的统一解析入口。该函数位于 parse_captures_compile.rs

pub fn parse(
    working_set: &mut StateWorkingSet,
    fname: Option<&str>,
    contents: &[u8],
    scoped: bool,
) -> Arc<Block> {
    parse_with_block_cache(working_set, fname, contents, scoped, true)
}

fuzz 目标调用时 fnameNonescopedtrue,内部委托给带 block 缓存的 parse_with_block_cache。也就是说,fuzz 器打的是与真实脚本执行完全相同的解析入口(含 block 缓存路径),而不是为测试单独抽出的简化接口——这保证了 fuzz 发现的路径问题在生产路径上同样成立。

Fuzz 工程的构建配置细节

fuzz/Cargo.toml 中有几处对理解本工程定位很重要的配置:

[package]
name = "nu-parser-fuzz"
version = "0.0.0"
publish = false
edition = "2024"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
nu-protocol.path = "../../nu-protocol"
nu-cmd-lang.path = "../../nu-cmd-lang"

[dependencies.nu-parser]
path = ".."

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

[profile.release]
debug = 1

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

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

逐项解读:

  • 独立 workspace[workspace] members = ["."] 且注释写明"防止干扰主 workspace"。这是 cargo-fuzz 的标准做法——fuzz 目标需要特殊的 #![no_main] 属性、独立的 profile 与 feature 组合,混入 Nushell 主 workspace 会导致主工程构建被污染。
  • 路径依赖直指被测 cratenu-parser 通过 path = ".." 直接指向上一级的 crates/nu-parsernu-protocolnu-cmd-lang 也以相对路径引入。这意味着 fuzz 永远针对当前检出状态下的最新源码运行,不存在版本漂移。
  • publish = falsetest = false / doc = false:fuzz 二进制不参与发布、不生成文档,两个 [[bin]] 声明也解释了为什么 cargo fuzz run 能按 parseparse_with_keywords 名字寻址目标。
  • [profile.release] debug = 1:保留一份调试信息,便于在触发崩溃时从 sanitizer 报告还原出有行号的堆栈。

运行建议与可验证的检查清单

  • 工具链前提:必须处于能使用 nightly Rust 工具链的环境(本工程已用 rust-toolchain.toml 声明);若本地未装 nightly,rustup 会在进入该目录构建时提示自动安装。
  • 种子语料是可选但推荐的起点:不运行 gather_seeds.nu 也可以裸跑 fuzzer(从零字节变异开始),但用真实 .nu 脚本做种子能显著加快发现深层解析分支上的问题。README 明确鼓励手动向 seeds/ 追加文件以提高多样性。
  • 产物位置:崩溃样本与精简语料落在你指定的 out/ 目录(本例为 crates/nu-parser/fuzz/out/),复现问题时可用其中的 crash 输入文件直接喂给普通解析逻辑验证。
  • 安全边界:两个目标都只解析、不求值;parse_with_keywords 可能触达关键字的 const eval 路径,但据 README 的当前评估,该命令集在 const eval 下无负面副作用。若未来 nu-cmd-lang 的核心命令集合发生变化,这一前提需要重新评估。

小结

Nushell 为 nu-parser 构建的这套 fuzz 工程麻雀虽小五脏俱全:gather_seeds.nu 用仓库自身的全部 .nu 脚本生成高价值种子语料;parse.rs 以空引擎状态压测纯词法/语法层;parse_with_keywords.rs 则通过 create_default_context() 注册核心关键字声明,把解析器更深处的关键字专用分支也纳入变异覆盖。两者共同把任意字节流送入 parse() 入口,在不执行任何用户代码的前提下,持续验证 Nushell 解析面对恶意或畸形输入时的健壮性。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384