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コンパイラ向けのトランスパイラを生成します。AIR(抽象中間表現)を、コンパイル時に静的解析を実行するZigソースコードに変換します。生成されたアナライザは、初期化前使用、解放後使用、スタックポインタのエスケープなどのメモリ安全性問題、および非null表明違反、タグ付きユニオン違反、fieldParentPtrの誤用などZig特有の未定義動作を検出します。
目標は、言語自体を変更することなく、AIRの静的解析を通じてZigにRustレベルのメモリ安全性保証をもたらすことです。
CLRは、フォークされたバージョンのZigコンパイラ(サブモジュールとしてzig/に含まれています)に依存しており、AIRを外部プラグインにルーティングする機能を追加しています。-ofmt=air -fair-out=<plugin.so>で呼び出すと、コンパイラは指定された共有ライブラリをロードし、生成されたAIRを処理のために渡します。
CLRは、すべての技術的に有効なZigプログラムを認識するだけでなく、リソースの状態が型と制御フロー構造で明示的かつ局所的に検証可能なライフサイクルパターンへプログラムを導くことを意図しています。2つの表現が可能な場合、CLRはリソース状態を型と制御フロー構造で可視化する方を優先します。
例えば、非オプショナルなファイル記述子を条件付きでクローズするのは避けてください:
const file = try std.fs.cwd().openFile(path, .{});
if (should_close) {
file.close(); // Bad: file is ambiguously open after this branch.
}
条件付き所有権はオプショナルで表現することを推奨します:
var file: ?std.fs.File = null;
if (should_open) {
file = try std.fs.cwd().openFile(path, .{});
}
if (file) |open_file| {
open_file.close();
}
非オプショナルな記述子を条件付きでクローズすると、ブランチ後にそのライフサイクルがあいまいになります。CLRの意図するポリシーは、永続的な"maybe closed"状態を持ち越さず、そのパターンを拒否することです。
同じ原則が割り当てられたポインタにも適用されます。派生ポインタを通じて解放しないでください:
const allocation = try allocator.alloc(u8, size);
const payload = allocation[header_size..];
allocator.free(payload); // Bad: payload is not the allocation base.
割り当てベースのポインタは解放用に保持し、派生ポインタはアクセスにのみ使用してください:
const allocation = try allocator.alloc(u8, size);
defer allocator.free(allocation);
const payload = allocation[header_size..];
use(payload);
フィールドポインタ、サブスライス、または算術演算によって生成されたポインタを解放することは、文書化された内部ルールが割り当てベースの由来を再確立しない限り拒否されます。
これらのポリシーはデフォルトで厳格です。なぜなら、よりシンプルでレビューしやすいリソースライフサイクルを持つコードを生成するからです。将来のunsafeアノテーションメカニズムにより、選択されたGIDや操作が個別の分析をオプトアウトできるようになります。これにより、プログラムの残りの部分に対するデフォルトモデルを弱めることなく、パフォーマンスのために弱いチェックを意図的に受け入れるコードをサポートします。
これは、元のElixirベースの概念実証をZigで積極的に書き換えたものです。Zig実装はコンパイラプラグインとしてロードされ、AIRを直接解析します。
現在実装済み:
std.mem.Allocatorインターフェースカバレッジ:
create/destroy - 単一アイテム割り当てalloc/free - スライス割り当て(alignedAlloc、allocSentinelなどを含む)realloc/remap - スライス再割り当て、古いスライスの解放追跡付きdupe/dupeZ - スライスの複製init/deinit/ - 完全なアリーナライフサイクル計画(詳細は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、タグハンドラ、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は有名な"unsafe"言語です。メモリ管理は手動で行われ、実装エラーの可能性が生じます。Zigは、安全性チェック付きコードで範囲外配列アクセスやヌルポインタ参照を排除することで、C言語に比べてセキュリティ問題を軽減しますが、静的解析により解放後使用、二重解放、データ競合を排除する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を参照してください。
allocatorstd.process.args、std.mem.asBytes、std.HashMap、アロケータ/ファイルAPIなどの一般的なパターン)std.HashMapの洗練(正規のメタデータ/キー/バリューストレージIDをput、get、getPtr、値イテレーション間で維持)posix.open/close/dup/dup2/socket/accept/epoll_create/pipe追跡