【ITニュース解説】Six patch releases in four days: how my merge gate kept failing open, and what fixed it
2026年10月15日に「Dev.to」が公開したITニュース「Six patch releases in four days: how my merge gate kept failing open, and what fixed it」について初心者にもわかりやすく解説しています。
ITニュース概要
AIが書いたコードを自動審査する「AdversaryGate」開発者が、意図的に攻撃し多数の脆弱性を発見した。テスト漏れや計測不備で誤ってコードが承認されるバグを修正するため、複数回のリリースを経てツールを強化した。
ITニュース解説
「AdversaryGate」というシステムは、開発者が作成した新しいコード(パッチと呼ばれる)を本番環境のシステムに組み込む際、その品質を自動的にチェックし、結合しても良いかを判断する門番のような役割を果たす。このシステムは、与えられたパッチに対して自動テストを実行し、問題がなければ「MERGE(結合許可)」、問題があれば「BLOCK(結合拒否)」、判断が難しい場合は「INCONCLUSIVE(判断不能)」という三種類の判定を下す。その目的は、バグや脆弱性を含むコードが誤ってシステムに組み込まれるのを防ぐことにある。
しかし、このAdversaryGateのバージョン3.0.0から3.0.6に至るまでの間に、本来「BLOCK」すべき変更を誤って「MERGE」と判断してしまう、多くの問題が発見された。開発者自身がシステムを「攻撃」し、どうすればすり抜けられるかを探すことで、これらの脆弱性が明らかになったのだ。これらのバグのほとんどは、「fail-open by omission」(見落としによる開けっ放し)という共通のパターンを持っていた。これは、AdversaryGateが特定の入力や状況を「測定対象外」と見なし、何も報告せずにそれをスキップしてしまうことで発生する。結果として、問題のあるパッチがゲートの目をかいくぐり、「MERGE」されてしまうという状況が生じた。システムの基本ルールである「誰も測定していないものは、合格ラインを超えられない」という原則が機能していなかったのである。
具体的な「fail-open」の事例とその修正について見ていこう。 まず、テストがコードの「意味」ではなく「形式」に騙されるケースがあった。例えば、あるテストが変更されたファイルのハッシュ値だけを比較することで、全ての変異(コードの一部を意図的に少しだけ変更したもの)が殺されたと誤って判断してしまう問題だ。これは、コードの末尾にコメントを追加しただけのパッチでもハッシュ値が変わるため、テストがパスしてしまうというものだった。この問題を解決するため、コメント変更だけを加えた「カナリア(Canary)」と呼ばれる特別なパッチを用いてテストを実施するようになった。もしこのカナリアパッチでテストが失敗するようであれば、そのテストはコードの内容を正しく評価できておらず、単にテキストの違いを読み取っていると判断され、測定結果は「測定不能」と報告される。さらに、コードの抽象構文木(AST)やバイトコード(コンピュータが直接実行できる形式に変換されたコード)を比較するテストも同様のトリックに騙されることが判明したため、カナリアはコメントだけでなく、コードの動作に影響しない「No-op(何もしない操作)」の代入文も挿入するようになった。これにより、コードの構造やバイトコードも変更されるため、より多くの不正なテスト手法を検出できるようになったが、まだ一部の小さなツリー断片を読み取るようなテストには不完全な部分も残っている。
次に、Gitのファイル管理がAdversaryGateを欺くケースも発見された。たとえば、.gitattributesという設定ファイルでPythonファイルが「バイナリファイル」としてマークされている場合、Gitの差分表示コマンド(git diff)は「Binary files ... differ」とだけ表示し、具体的なコードの変更内容を一切報告しなかった。これにより、AdversaryGateはコード変更を認識できず、カバレッジ(テストによって実行されたコードの割合)も計測されないまま、問題のないパッチとして「MERGE」を許可してしまった。この問題は、そのようなgit diffの出力が検出された場合、そのパッチの結合を拒否するように修正された。
さらに、ドキュメントがコードと誤認される問題もあった。READMEファイルのようなドキュメントが、システムのコード行数カウントに含められていたため、コードの変更が一切ないパッチであっても、READMEファイルの行を削除するだけでカバレッジが1.0と判定されてしまうことがあった。これは、本来意味のない変更でテストが合格したと誤解され、「MERGE」が許可される事態を招いていた。この問題は、コード行を一切含まないパッチの場合、カバレッジは「未測定」として扱われるように修正された。カバレッジが未測定の場合、「MERGE」は不可能となるため、このような誤判定は防げるようになった。
テストの網羅性に関する問題もいくつか見つかった。一つは、条件分岐がそのガード(条件式)と同じ行に記述されている場合(例: return 999 if x == 0 else x + 1)、変異テスト(コードの一部を意図的に変更してテストの品質を評価する手法)がその特定の分岐を見落とし、全ての変異が殺されたと誤って判断されてしまう問題だ。これにより、実際にはテストされていないコードがあるにもかかわらず、「MERGE」が許可されていた。この修正として、AdversaryGateはこのような分岐に対する変異を生成するようになり、もし変異が生き残れば、そこが「死角」と判断され、「INCONCLUSIVE」という判断不能な結果を出すように変更された。また、変更されたコードの最初の12行しか変異テストの対象になっておらず、それ以降の変更が見過ごされていた問題も修正され、変更された全ての行が最低1回は変異テストされるようになった。
モジュールの誤認識も問題となった。AdversaryGateは、モジュールがテストコードであるか否かを、そのファイルパスのみで判断していた。そのため、テストコードのような名前を持つが、実際には製品コードからインポートされて使用されるモジュールが存在した場合、それはテストコードと誤認され、カバレッジや変異テストの対象外とされていた。このモジュールへの変更はAdversaryGateによって全く評価されずに「MERGE」される可能性があったのだ。この脆弱性は、テストのような名前を持つモジュールであっても、非テストコードからインポートされている場合は、通常のソースコードとして扱い、適切に評価されるように修正された。ただし、importlibや__import__のような動的インポート経由で読み込まれるモジュールは、まだ検出できないという限界も存在する。
「fail-open」以外にも、正当なパッチが不当に「INCONCLUSIVE」(判断不能)と判定される問題も修正された。例えば、ファイル末尾の改行なしを示すマーカーが1行とカウントされて行番号がずれること、テスト実行に設定された時間制限(900秒)がフルテストスイートの実行時間よりも短かったこと、Android環境(Termux)でのデフォルトメモリ制限が厳しすぎてプロセスが強制終了すること、といった環境要因や設定ミスが原因で、適切なコードが評価できない状況があった。これらは全てバージョン3.0.5で修正された。
リリースプロセス自体も改善された。以前のリリースでは、APIトークンの取り扱いミスや、PyPIパッケージの信頼性を保証するアテステーション(署名による保証)が不足していた。現在では、リリースは署名付きタグで行われ、信頼できる公開プロセス(Trusted Publishing)を経由することで、PyPIパッケージには公式のアテステーションが付属するようになっている。これにより、パッケージの改ざんリスクが低減された。過去の脆弱なバージョンはPyPIとGitHubから全て削除された。
AdversaryGateにはまだ限界もある。現在、コードの測定はPython言語とpytestというテストフレームワークにのみ対応しており、他の言語のコードは常に「INCONCLUSIVE」と判断される。また、同じOSユーザー権限で動作するコードエージェントが、ゲートが一時的に使用するディレクトリにアクセスするのを防ぐことはできない。このセキュリティ境界はドキュメントで説明されている。さらに、前述のカナリアテストによる修正もまだ完全ではなく、一部の抜け道が存在する。これらの限界については、各修正の隣に詳細が記述されている。
AdversaryGateで発見された全てのバグは、公開されている台帳に、その再現方法と修正内容が詳細に記録されており、システムの透明性が高く保たれている。もし最新版の3.0.6で、本来「BLOCK」すべきパッチを誤って「MERGE」と判断させるような脆弱性を見つけられた場合は、プライベートな方法で報告することが奨励されている。