HN 日本語サマリー

← 一覧へ戻る
プログラミング

不安定なテストがRedisクライアントのuse-after-freeを露呈

A flaky test exposed a Redis client use-after-free (buildkite.engineering)

32 pointsby DrNemski10 コメント

要約

Buildkite社は、不安定なテストが原因でRedisクライアントライブラリ内のメモリ破損バグを発見しました。このバグは、テスト環境だけでなく開発環境でも発生しており、最終的にASAN(AddressSanitizer)とコアダンプ解析により、RedisクライアントのC拡張におけるuse-after-freeであることが特定されました。この問題は、テスト環境でのC拡張の無効化と、バグ報告のupstreamへの提出によって解決されました。

全文翻訳

バグを修正するのは、それが決定的であっても難しいことがあります。それを引き起こす状況を再現することが、しばしば最も難しいステップとなります。バグが非決定的である場合、これはさらに難しくなります。その動作を観察するために、それを再現するコードを何度も実行する必要があり、フィードバックループにかなりの時間がかかります。さらに難しいのは、バグの原因がその顕現よりも前に発生する場合です。データが破損しても、すぐにそれに気づかない場合です。私たちの通常のツールは、クラッシュが発生した時点でのシステムの状態をキャプチャすることに焦点を当てていますが、なぜそもそも破損が発生したのかは教えてくれず、ただ発生したことだけを伝えます。これは、redis-clientライブラリで見つけたメモリ破損バグと、それにたどり着くのに役立ったことについての物語です。 第1章: バウンスジャンプ!バウンス!ダウン!アップ! 私たちの物語は、スケーラビリティチームのエンジニアリングマネージャーであるDavidから始まります。あるのんびりした火曜日、Davidは、一連の不安定なテストが原因でビルドが失敗したのを見ました。彼はSlackで他の人に警告するスレッドを開始し、Test Engineダッシュボードからのスクリーンショットを含めました。継続的インテグレーション企業として、私たちは不安定なテストには精通しています。それらは、どんな大規模なコードベースでも避けられない部分です。15分以内に、チームはアンドンコードを引きました。これらのテストは非常に不安定で、コードを本番環境にデプロイするのを妨げていました。Davidが要約したように、「[最優先事項は]メインのブロックを解除すること」だったので、私たちは一時的にテストをスイートから除外して作業しました。緊急性が低下したことで、この障害によってブロックされていたDavidや他の多くの開発者は、元のタスクに戻ることができました。しかし、Davidは、BuildkiteのTest Engineの信頼性スコアを使用してこれらのテストの信頼性を検査することで、もう少し深く調べることにしました。それらが突然不安定になった時期を相関させることができるかどうかを確認するためです。最も不安定だった3つのテストは、約2〜4日前に100%近く信頼できていた状態から…そうではなくなったようです。変更に寄与した可能性のあるものを特定するために、マージされたPRを調べましたが、明白なものはありませんでした…唯一目を引くのは、Redisのアップグレードです?そのRedisのアップグレードは、前の金曜日の午後に、スタッフエンジニアであり、パートタイムのブログ語り手であるPatrick Robinsonによって行われました。Patrickは、自分のことについて三人称で話すのが大好きですが、その中でも、厄介なバグに深く入り込むことが好きなのです。ここに最初の手がかりがあります。Redis gemのアップグレードは、本番環境の問題の兆候もなくスムーズに進んでいました。しかし、この時点で、第二の仮説が現れました。テストは、特定のイベントシーケンスに依存していたために不安定だったのではないかということです。というのも、不安定なテストは私たちの機能テストスイートの一部であり、RSpecだけでなく、Seleniumヘッドレスブラウザ、そしてWeb/APIサーバー、データベース、Redisサーバーからなるテスト環境も利用していたからです。このような状況では、異なるコンポーネント間の競合状態が、不安定なテストの一般的な原因となります。残念ながら、これは調査者を誤った方向に導くことになります。そのため、提案されたのは、テストのセットアップとアサーションの間にsleepを挿入して、問題が解消するかどうかを確認することでした。これは解決策ではありませんでしたが、不安定さがWebSocketメッセージの到着が少し遅れた結果であるかどうかを判断する方法でした。 第2章: フェード・トゥ・ブラック 人生は、消えゆくように見える 日々、さらに遠くへ 自分自身の中で迷子になり 何も重要ではない、他の誰もいない 翌月曜日、同じファイルで、スキップされたテストと同じように、さらに多くのテストが散発的に失敗していました。Patrickは、Patrickとは異なり、実際のフロントエンドコードを書くことができる別のスタッフエンジニアと協力しました。一緒に、彼らはバグの信頼性の高い再現を構築することに成功しましたが、失敗を発生させるのに最大30分かかりました。テストスイートが実行された後にメッセージが配信される競合状態の可能性を排除した後、チームはActionCableとRedisの間の相互作用に焦点を絞りました。追加のロギングを追加することで、subscribeコマンドがActionCableに配信されているがRedisには受信されていないことを発見しました。他の開発者も、開発中にWebSocketが頻繁に失敗すると報告しており、テスト環境だけでなく影響を受けていることが示唆されました。デバッグの3日目の終わりに、何も進んでいないように見えたとき、Patrickは非常に珍しいエラーメッセージを受け取り、それをSlackに「今日はもう諦める時間だと思う…」という言葉とともに投稿しました。 textruby(29632,0x2a9f1f000) malloc: Double free of object 0x2a9bb6740 ruby(29632,0x2a9f1f000) malloc: *** set a breakpoint in malloc_error_break to debug この種の例外は非常に珍しく、このバグがどれほど深いかを示唆しています。しかし、チームは、それが失敗とどのように関連しているか、あるいは関連しているのかさえ認識するのに十分な情報をまだ収集していませんでした。大ヒット映画シリーズ「ナイブズ・アウト」のエピソードのように、すべての情報があっても、それはまだ理解できませんでした。そして、すべてのピースがテーブルの上にあるにもかかわらず、この犯罪は本当に不可能に見えます。 ~ ベノワ・ブラン 第3章: エヴァーロング こんにちは あなたを待っていました エヴァーロング 成功したデバッグには、かなりのスキルと少量の運が必要です。翌週の木曜日の午後遅く、エンジニアリングSlackチャンネルに、ビルドがセグメンテーションフォールトを引き起こしたというメッセージが投稿されました。ビルドにはコアダンプが添付されていました。コアダンプが添付されたのは、ある開発者が以前テストスイートでのクラッシュをデバッグしようとしており、コアダンプの存在を探し、見つかった場合は関連ビルドにアーティファクトとしてアップロードするプラグインを書いていたからです。これは、3人目のキャラクター、Rianを紹介する時です。彼はBuildkiteでの在職中に、信じられないほど難解なバグ(悪名高いDoomパイプラインとともに)を見つけて修正する評判を築きました。その話は非常に素晴らしく、Rianは去りましたが、私たちは新入社員にそれらを語り、彼らはそれを他の人に語り、そしてそう続きます。 コアダンプにより、Rianはクラッシュ時のプロセスの状態を、航空機墜落事故調査におけるブラックボックスのように検査することができました。クラッシュメッセージ自体は、それがhiredisコード内で発生したことを示していました。これは、redis-client gemにバンドルされているC拡張機能です。そこから、彼は例外がC関数memmoveの呼び出しによってトリガーされたことを確認しました。ソースと宛先のアドレスはこの呼び出しに対して妥当に見えましたが、サイズフィールドは0x00a0ffffffffffb6に設定されており、これは約45ペタバイトです。これと、Redis gemをアップグレードして以来、他の予期しない動作を目にしていたという事実に基づいて、最新バージョンにメモリ破損バグが存在すると結論付けるのは論理的でした。 また、幸運にも、Rianは以前に、ASANを使用したC言語でのメモリ安全性バグの発見と修正というカンファレンストークに参加していました。この情報を使って、RianはASANでコンパイルされたRubyのバージョンで再現可能なコードを実行する作業に取り掛かりました。それを実行するのにわずか数時間しかかからず、30分以内にASANはヒープのuse-after-freeを報告しました。並行実行を提供するために、hiredisは接続ごとに2つのスレッドを持っています。1つは読み取り用、もう1つは書き込み用です。use-after-freeは、一部のシナリオでリーダー(reader)スレッドがライターバッファ(writer buffer)を解放するために発生していました。 エピローグ: 結局、ここまで進んだことに驚いています 物事は以前のようではありません あなたは私を認識することさえしないでしょう 当時私を知っていたわけではありませんが、結局すべてが私に戻ってきます メモリ破損があり、さらに重要なことに、それがどこから来ているのかを知ったとき、修正は比較的簡単でした。Rianは、テスト、開発、および本番環境でRedis接続のC拡張機能の使用を無効にし、upstreamに報告された再現可能なテストケースを開発しました。そもそもそれを見つけることができたのは興味深い部分です。必要なツールを構築する上でのスキルの意図的な適用、チームワーク、そしてちょうど良い量の運の組み合わせです。Test Engineやcoredump-artifactプラグインなどのツールは、バグの性質を特定することを可能にし、ASANの使用と組み合わせることで、それがどこにあるのかを特定しました。