
2026/10/09 16:29
ニックスが私のデバッガーの半分を書いた
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Rewind VM は、Nix ビルドをその入力(スレッドスケジューリングを含む)の純粋な関数として扱い、ローカルストアおよびバイナリキャッシュに対してハッシュベースのチェックによる正確な再現性を検証することで、仮想マシンデバッグを決定論的に行います。このシステムは、ハッシュ付きクロージャーを含む読み取り専用 erofs イメージ上で起動します。ビルド ID は cache.nixos.org または
debuginfod を介してデバッグ情報(Linux カーネルソースを含む)を取得するために使用されます。分析を支援するために、Rewind はソースパネル、スタックフレーム、.rwd エクスポートファイルに保存されたブックマーク、「Compare」タブで並置表示され最初に異なるイベントをハイライトするトレース比較機能、ゲストカーネルが各ステップで CPU 所有権を報告できるようにして実行を正確に再プレイするためのツールを提供します。rewind gdb を使用すると、すべてのスレッドを維持したまま VM の任意のステップでフォークできます。「Compare」タブと rewind compare は別々の実行間の差異を表に出し、rewind check --run は複数のスケジューリング下で任意のステップから実行をフォークして競合条件のインターリーブ可能性を評価します。この無料のデバッグインフラストラクチャ(入力、シンボル、ソース、決定論的分析機能)を提供することで、Rewind は高価な独占ツールなしで並行性问题への調査の障壁を下げてソフトウェアの信頼性を向上させます。本文
Nix 導出を基盤とした Rewind VM:Race Condition 検知のための超能力
概要
数日前に提唱した Rewind VM は、入力(スレッドスケジューリングを含む)のみを受け入れる純粋関数として動作する決定論的な仮想マシンです。 このツールを用いて、Nix ビルドにおける多数の**実行順序依存バグ(Race Condition)**を発見、再現し、解決しています。
- 機能の急速な拡張: ソースパネル、スタックフレーム、ブックマーク、「Compare」タブ、GDB 機能への対応など。
- 驚異的な実装: 「大量のコード改修が必要」と考えがちだったが、実装の困難な部分は既に Nix 自身が提供していた。
- デバッガー動作に必要な入力・シンボル情報・ソースコード・ライブラリ・環境再現方法などがすべて「Derivation(導出)」としてシンプルに確保できるため。
なぜ Rewind は Race Condition を浮き彫りにするのか?
銀行口座の例:典型的な Race Condition
2 つのスレッドが 1 つの口座にお金を預ける場合、以下の手順で競合が発生します。
- 残高を読み込む(
)。seen - ログに書き込む。
- 残高を更新する(
)。balance = seen + amount
片方のスレッドが他者の「読み込み」と「保存」の間に実行されると、陳腐化されたデータを書き込んでしまい、資金が消失します。
// bank.c:24 - 簡易的な deposit 関数 static void deposit(int teller, long amount) { long seen = balance; // [読み込み] char line[64]; int n = snprintf(...); // ログ出力 if (write(1, line, n) != n) return; balance = seen + amount; // [保存] }
- 現実での失敗: 16 コア搭載のノート PC で 1,000 回試行した際、396 回で資金が失われました。
- 単一コアでの検証:
を使って単一コアに固定すると、資金損失は発生しませんでした(スレッド切り替えが稀ため)。taskset - Rewind の役割: 意図的にスケジュールを乱すことで、1 回でもデポジットの途中で干渉するケースを確実に浮き彫りにします。
再スケジュールによる問題特定
Rewind VM は単一 CPU しか持たないため最初の実行は成功しますが、
rewind check コマンドが異なるスケジュールで再度実行し、失敗を特定のステップに絞り込みます。
$ rewind check --where github:fzakaria/rewindvm#bank ... step 3237 decides it: a reschedule there makes the run fail passing: run 12feb5f832205a72, schedule 0 failing: run c2f1cfac0c288f3e, schedule 1 over steps 3237..3238 the two are the same run until step 3237
このチェックは私のノート PC でわずか 11 秒で完了しました。ステップ 3237 まで完全同一であり、その時点で失敗例だけが再スケジュールの影響を受けます。
【無料機能】入力情報による完全な再現
rewind nix は Derivation の入力を解決し、読み取り専用の EROFS イメージにパックして VM を起動します。
- 実行 ID: 入力からのハッシュで生成(Store パスの生成方法と同様)。
- 復元コマンド:
でその入力を再現するコマンドを取得できます。rewind show
$ rewind show 12feb5f8 rewind nix /nix/store/cr8rl40rjb9cmmcd9sxn9mpdrhvp53jj-bank-0.1.0.drv --epoch 1791331200 --clock branches --name bank-0.1.0
- 検証: VM 内のビルドがホスト上のビルドと同一であることを、
とバイナリキャッシュの比較で確認できます。.narinfo
【無料機能】すべてのシンボルとソースコード
- ソース表示: アプリパネルまたは
コマンドで、プレイヘッド(カーソル)位置のソースコードとスタックフレームを表示。rewind where - Nix の恩恵:
がnixpkgs
でビルドされており、デバッグ情報はキャッシュされているため、**あらゆるバイナリ(Linux カーネル含む)**のソースとシンボル情報を無料で取得可能。separateDebugInfo
$ rewind where c2f1cfac 3250 process 139 (bank), thread 141, at step 3250 #4 deposit (bank.c:24) 22 int n = snprintf(line, sizeof line, "teller %d: %ld + %ld\n", teller, seen, amount); 23 > 24 if (write(1, line, n) != n) 25 return; 26 balance = seen + amount; called from #5 teller (bank.c:34) called from #6 start_thread (pthread_create.c:454)
GDB をステップ単位でフォークして利用する
ソースコードだけでは不十分な場合、Rewind GDB でプレイヘッドをフォークし、GDB を起動できます。
- 機能: ブレークポイント・ウォッチポイントの設定、メモリー・レジスタ・変数の検査が可能。
- 利点: GDB の操作は記録に影響せず、同じステップに戻って再度フォークするか、他のステップでフォークしても同じ状態が再現できます。
$ rewind gdb <run-id> # 例:balance 変数を監視し、変更されるまで続行 step 3249 (teller 2 が CPU を得る直前) -> watch balance
より詳細なシステムコール追跡が必要な場合は、
strace パッケージをパックしたシェルも起動可能です。
二つの実行を瞬時に比較する
「Compare」タブまたは
コマンドを使用すると、両方の実行の共有イベントと最初の違いが即座に表示されます。rewind compare
- 比較表示: 分岐直前のイベントを並列表示し、差異部分をマーク。
- 例: 失敗実行(teller 2 は 150 から開始)vs 成功実行(200 から開始)。
CPU を保持したのは誰か?:ギャップ埋め機能
レース条件は「どのスレッドがいつ実行されたか」の順序問題です。 通常のトレースは「スレッドが何をしたか」しか記録せず、「隙間(CPU が誰に持たれたか)」は省略されます。
Rewind VM は新しい記録を追加せずに、これらのギャップを埋められます。
- 原理: 各実行を完全リプレイし、任意のステップで「ゲストカーネルに確認」することで、その時点の CPU ホルダーを特定。
- 検出例: ステップ 3238 で teller 1 が中断され、teller 2(スレッド 141)が単一ステップで CPU を得て長い間読み込みを行うことで問題が発覚。
ここからチェック(Check from here)
「ある特定の失敗が偶然か、再現性のあるバグか?」を検証します。
rewind check --run フラグを使用すると、任意のステップからの分岐で、異なるスレッド順序を数多く試すことが可能です。
$ rewind check --run 12feb5f8 --schedule-from 3221 --schedules 16 --all --no-narrow ... schedule 0: exited:0 4528 steps dbf0df3de84b run 12feb5f832205a72 # 成功 schedule 1: exited:2 3313 steps run f6eb462913dce489 # 失敗 ... schedule 16: exited:2 3314 steps run 63f7817972a90273 # 失敗 16 of 16 perturbed schedules ended differently
この結果により、すべてのスケジュールが異なる結果に終わり、「運が良い」場合ではなく確実なバグであることが証明されます。 Rewind アプリのコンテキストメニュー「Check from here」でも同様の操作が可能です。
ブックマーク機能
プレイヘッドのステップに注釈(
b コマンド)を付与し、ブックマークできます。
- 永続性: 実行とともに保存され、
エクスポートファイルに含まれるため、他人に共有した際にも注釈が再現可能。.rwd
さらに試せるサンプルケース
各例は
flake 内の derivation であり、「バグ」と「修正」の両方を検証できます。
- philosophers: デッドロック問題。
- bank: 上記の更新損失問題。
- waiter: フラグチェックと
の間にpause
が到着し、親プロセスがスリープを続行してタイムアウトで殺害されるケース。SIGCHLD$ rewind check github:fzakaria/rewindvm#waiter - config-reload: 1 つのプロセスが config ファイルを書き換えつつ、別のプロセスで再読み込みを行う例。複数コアではほぼ常に失敗するが、単一 CPU では特定のスケジュールのみで失敗(reader が truncate と最後の write の間に位置するため)。
Nix は超能力です
デバッガーにおける多くの困難な作業は、Nix によって既に解決されています。 Rewind はこれに加えて「意図的な乱す」機能を提供するだけで、以下のようなインフラストラクチャを無料で提供しています:
- 完全な入力情報: ソースコード・コンパイラ・ライブラリ・環境構成。
- シンボル情報: カーネルを含むすべてのバイナリのデバッグ情報。
- 再現性: 他者が自分のマシンで環境を完全に再現するための導出。
つまり、Nix Derivation のように閉鎖されたものから始めれば、完全なデバッガー環境は「無料」で得られます。
$ nix run github:fzakaria/rewindvm -- check github:fzakaria/rewindvm#bank