9月8日、Project Zero が「Testing race conditions with memory access tracing and stack-based delay injection」と題した記事を公開した。この記事では、Linuxカーネルのレースコンディションをメモリアクセストレースと遅延注入によって再現・検証するツール群の設計と実装について詳しく紹介されている。以下に、その内容を紹介する。
レースコンディションは、セキュリティバグの中でも特に扱いにくい部類に属する。複数スレッドが特定の順序で実行された場合にのみ問題が顕在化するため、バグの存在を「証明する」テストを書くこと自体が難しい。パッチ後のリグレッションテストも同様で、再現性を保証する手段がほぼない。
Googleのセキュリティ研究チームProject Zeroのエンジニアがこの問題に正面から向き合い、MAccConc(Memory Access Concurrency)というツール群を開発・公開した。GitHub上で公開されており、以下の3つのコンポーネントで構成される。
- 自動A-B-A割り込みテスター:テストケースの全可能なA-B-A実行順序を自動で試す
- ターミナルUI:実行順序を手動探索するためのTUI
- GUI:同じく手動探索向けのグラフィカルUI
核心アイデア:「カウント付きスタックトレース」による安定的な特定
このツールの最も面白い部分は、複数回の実行にわたってメモリアクセスを安定的に同定する仕組みだ。
単純にデータアドレスで特定しようとすると、毎回の実行でオブジェクトが異なるアドレスに確保されるため機能しない。命令アドレスだけでも、memcpy()やspin_lock()のような汎用関数内では区別がつかない。
Project Zeroのエンジニアが採用したのが「カウント付きスタックトレース(count-augmented stack traces)」だ。各スタックフレームに「この関数へのN番目の呼び出し」というカウントを付与することで、実行トレース上の位置を一意に特定する。
記事中の例を引用する:
"On this thread, look at the second call to
__x64_sys_recvfrom, then within that, the first call to__sys_recvfrom, then within that the first call tosock_recvmsg, then within that, the first call tounix_stream_recvmsg, then within that, the first call tounix_stream_read_generic, then within that, the second call to_raw_spin_unlock, and then within that, the first memory access at instruction address X"
この識別子は具体的なデータアドレスに依存せず、無関係なコードのフロー変化にも比較的影響されない。
この仕組みを実現するため、KCOVに関数エントリ/イグジットイベントの提供を求める必要があった。エンジニアはそのためのLLVMパッチを開発し、LLVM 23.1.0でリリース済みだ(該当コミット、ドキュメント)。
メモリアクセスの収集:ASANアウトラインモード
メモリアクセスの収集には、ASANの「アウトライン」モード(コンパイラフラグ asan-instrumentation-with-call-threshold=0、Linuxカーネルでは CONFIG_KASAN_OUTLINE)を利用する。これによりメモリアクセス毎にヘルパー関数が呼ばれる。
トレースデータの格納先として、既存のKCOV(カーネルのコードカバレッジ情報をユーザー空間に渡す仕組み)を流用している。ftraceも選択肢だったが、KCOVはインメモリ表現がシンプルで、クラッシュしたVMからのデータ回収にも有利なため採用した。
注意点として、ASANはスタック上の直接アクセスにはヘルパーを生成しない(範囲外アクセスの可能性がある場合を除く)。そのため、wait queueのようなスタック上オブジェクトが絡むレースコンディションは検出できない場合がある。
遅延注入:実行順序を強制する
特定した通信ポイント(communication points)に対して実行順序を強制するため、KCOV_SET_DI ioctlを実装した。アクション種別は以下の3つだ:
DI_STACK_WAKE_PRE:メモリアクセスの前にフラグNをセットDI_STACK_WAIT:メモリアクセスの前にフラグNがセットされるまでスピン待機DI_STACK_WAKE_POST:メモリアクセスの後にフラグNをセット
実際の動作:dup(5) と close(5) の競合
デモとして、dup(5) と close(5) を同時実行するテストケースに自動テスターを適用した例が示されている。
void test_thread1(void) {
dup_res = dup(test_fd);
dup_errno = errno;
}
void test_thread2(void) {
close(test_fd);
}
実行結果の一部:
stats: injection-failed:0 wait-timeout:7 reordered:4
複数の実行結果の中に dup(5) = 5 (success) というケースが現れており、これは「closeされたfdと同じ番号でdupが成功する」という一見意外な動作を示している。バグではないが、競合状態での挙動として注目に値する結果だ。
関連先行研究
このプロジェクトはUsenix OSDI 2014で発表されたSKIと思想的に近い。SKIはQEMUのTCGモードを改造してメモリアクセス記録とvCPUスケジューリング制御を行うが、MAccConcはASAN計装とKCOVを用いてカーネル内で完結させる点が異なる。ベアメタルハードウェア上での動作も理論上は可能だ。
また、Project ZeroのNed Williamsonによるsockfuzzerの並行性テストのアプローチからもインスピレーションを得ている。
詳細はTesting race conditions with memory access tracing and stack-based delay injectionを参照していただきたい。