Checker of
Lifetimes and other
Refinement types
for Zig
视频:https://www.youtube.com/watch?v=mf0WzTOe-40
赞助:https://buymeacoffee.com/dnautics
在 HN 上讨论:https://news.ycombinator.com/item?id=42923829
在 lobste.rs 上讨论:https://lobste.rs/s/9sitsj/clr_checker_for_lifetimes_other
现场演示视频:https://www.youtube.com/watch?v=ZY_Z-aGbYm8
该项目为 Zig 编译器创建了一个 Zig 转译器,它将 AIR(抽象中间表示)转换为在编译时执行静态分析的 Zig 源代码。生成的分析器可以捕获内存安全问题,例如:未赋值即使用、释放后使用、栈指针逃逸,以及 Zig 特有的未定义行为(如非空断言、联合体(tagged union)违规或 fieldParentPtr 误用)。
目标是通过对 AIR 的静态分析,将 Rust 级别的内存安全保证带到 Zig,而不改变语言本身。
CLR 依赖于一个分支版本的 Zig 编译器(作为子模块包含在 zig/ 中),该版本增加了对外部插件路由 AIR 的支持。当使用 -ofmt=air -fair-out=<plugin.so> 调用时,编译器会加载指定的共享库,并将生成的 AIR 传递给它进行处理。
CLR 旨在推动程序采用显式且局部可验证的生命周期模式,而不仅仅是识别每一个技术上有效的 Zig 程序。当两种表示方式都可行时,CLR 更倾向于使资源状态在类型和控制流结构中可见的那种。
例如,避免有条件地关闭一个非可选的(non-optional)文件描述符:
const file = try std.fs.cwd().openFile(path, .{});
if (should_close) {
file.close(); // 差的做法:文件在此分支后处于模糊的打开状态。
}
推荐使用可选类型表示条件所有权:
var file: ?std.fs.File = null;
if (should_open) {
file = try std.fs.cwd().openFile(path, .{});
}
if (file) |open_file| {
open_file.close();
}
有条件地关闭一个非可选的描述符会使其生命周期在分支后变得模糊。CLR 的预期策略是拒绝这种模式,而不是永久携带一个“可能已关闭”的状态。
同样的原则适用于已分配的指针。不要通过派生指针进行释放:
const allocation = try allocator.alloc(u8, size);
const payload = allocation[header_size..];
allocator.free(payload); // 差的做法:payload 不是分配基地址。
保留分配基地址指针用于释放,仅使用派生指针进行访问:
const allocation = try allocator.alloc(u8, size);
defer allocator.free(allocation);
const payload = allocation[header_size..];
use(payload);
释放字段指针、子切片或通过算术运算产生的指针,除非存在文档化的内部规则重新建立分配基地址的来源,否则将被拒绝。
这些策略默认是严格的,因为它们能产生资源生命周期更简单、更易审查的代码。未来的不安全注释机制将允许选定的 GID(全局标识符)或操作选择退出个别分析。这将支持那些有意接受较弱检查以换取性能的代码,同时不削弱程序其余部分的默认模型。
这是对最初基于 Elixir 的概念验证项目在 Zig 中的积极重写。当前的 Zig 实现作为编译器插件加载,并直接分析 AIR。
目前已实现:
std.mem.Allocator 接口覆盖:
create/destroy - 单项分配alloc/free - 切片分配(包括 alignedAlloc、allocSentinel 等)realloc/remap - 切片重新分配,带旧切片已释放跟踪dupe/dupeZ - 切片复制init/deinit/allocator - 完整的 Arena 生命周期计划中(详见 LIMITATIONS.md):
sudo apt install bats供应商提供的 Zig 编译器和 libclr 插件必须以匹配的优化级别构建。优化级别不匹配会导致段错误。
# 构建自定义 Zig 编译器,使用 ReleaseFast(仅在第一次或子模块更改后执行)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseFast && cd ..
# 构建匹配优化的 CLR 插件
zig build -Doptimize=ReleaseFast
对于开发/调试,可以将两者都设置为 ReleaseSafe 或 Debug:
# ReleaseSafe(带有安全检查,稍慢)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseSafe && cd ..
zig build -Doptimize=ReleaseSafe
# Debug(完整的调试信息,最慢)
cd zig && zig build --zig-lib-dir lib && cd ..
zig build
# 使用 AIR 后端编译一个 Zig 文件
zig/zig-out/bin/zig build-exe -fair-out=zig-out/lib/libclr.so -ofmt=air -femit-bin=output.air.zig your_file.zig
# 运行生成的分析器
zig run --dep clr -Mroot=output.air.zig -Mclr=lib/lib.zig
输出发送到 stderr。
# 单元测试(代码生成/DLL)
zig build test
# 单元测试(运行时库)
zig test lib/lib.zig
# 一个聚焦的集成测试文件
bats test/integration/fd.bats
# 集成测试(需要 BATS)
# 默认为 ReleaseFast;可覆盖为 OPTIMIZE=ReleaseSafe 或 OPTIMIZE=Debug
./run_integration.sh
# 手动测试单个文件
./run_one.sh test/cases/undefined/use_before_assign.zig
注意: 集成测试会使用指定的优化级别(默认:ReleaseFast)重新构建 libclr。确保您供应商提供的 Zig 编译器是以匹配的优化级别构建的。
clr/
├── src/ # DLL/插件代码(生成 .air.zig)
│ ├── clr.zig # CLR 插件主入口点
│ ├── codegen.zig # 从 AIR 指令生成 .air.zig 源代码
│ └── allocator.zig # DLL 安全的分配器包装
├── lib/ # 运行时分析库
│ ├── lib.zig # 库入口点
│ ├── tag.zig # AnyTag 联合体, Type, tag 处理器, splat 分派
│ ├── Inst.zig # 指令结果和过程间分析
│ ├── Refinements.zig # 精化类型(指针、结构体、可选等)
│ ├── Analyte.zig # 分析状态容器
│ ├── Context.zig # 执行上下文(元数据,错误报告)
│ └── analysis/ # 分析模块
│ ├── undefined_safety.zig # 未赋值即使用跟踪
│ ├── memory_safety.zig # 分配/释放跟踪
│ ├── null_safety.zig # 可选解包检查
│ ├── variant_safety.zig # 联合体字段访问
│ └── fd_safety.zig # 文件描述符跟踪
├── test/
│ ├── integration/ # BATS 集成测试
│ │ ├── test_helper.bash
│ │ └── *.bats
│ └── cases/ # 测试输入文件 (.zig)
├── zig/ # Zig 编译器子模块(已插桩的分支)
├── build.zig # 构建配置
└── build.zig.zon # 包依赖

Zig 是一种著名的“不安全”语言。内存管理是手动的,这导致了实现错误的可能性。尽管 Zig 通过消除安全检查代码中的越界数组访问和空指针解引用,相比 C 减少了安全问题,但它仍然不如 Rust 安全,因为 Rust 通过静态分析消除了释放后使用、双重释放和数据竞争。
受 Rust 的 MIRI 项目启发,CLR 对 Zig 的 AIR 中间表示执行静态分析,以实现比 Zig 开箱即用更高的安全性。与 MIRI 在沙箱伪运行时中解释 Rust 的 MIR 不同,CLR 将 AIR 转译为静态运行分析的 Zig 源代码。请注意,CLR 的 AIR 输出的 Zig 代码原则上可以在编译时运行,但通过 Zig 中间层,我们产生了一个易于理解和调试的逻辑流程。有抱负的人可能会使用这种通用方法输出不同的目标,例如证明助理语言,或者重构它使其完全在 Zig 编译器中运行!
关键洞察:如果即使是最注重安全的 Rust 项目也需要 MIRI,那么为什么不选择一种更简单的语言,并执行 MIRI 风格的分析来获得借用检查和其他精化类型分析呢?这个项目表明,这样的未来对于 Zig 是真实可能的。
Zig 编译管线为:
AIR 是进行分析的理想层次,因为它是带类型的,可以被解释为广义编程指令的“最小可行”列表,并允许用精化元数据扩展类型。
关于 Zig 的 AIR 如何工作的深入分析,请参见 Mitchell Hashimoto 的博客文章:https://mitchellh.com/zig/sema
MIT 许可证 - 详情请参见 LICENSE。
std.process.args、std.mem.asBytes、std.HashMap 以及分配器/文件 APIstd.HashMap 精化,包含规范的元数据/键/值存储恒等性,横跨 put、get、getPtr 和值迭代posix.open/close/dup/dup2/socket/accept/epoll_create/pipe 跟踪