
バイナリインストルメンテーションとファジングによって発見されたいくつかのバグ
media-gen は、2つのメディアパーサーテストケースを構築するための、依存関係の少ない小さな Rust コマンドラインツールです。既存の有効なメディアファイルをその場で編集します。FFmpeg を呼び出したり、ブラウザにパッチを当てたり、サーバーに接続したり、何かをアップロードしたりはしません。
WebM ケースには、旧来の1バッファ遅延 Vorbis パスと現在の即時破棄パスの両方に対応するブリッジレイアウトが含まれています。チェックインされている短いサンプルは、Discord Desktop 1.0.9257 (Electron 42.11.1 / Chromium 148.0.7778.280) および Chrome 153.0.8010.48 で検証済みです。M4A ケースは、固定された Discord/FFmpeg ビルドに固有のままです。他のリリースでは、これらのファイルを拒否したり、安全に処理したり、異なる形で失敗したりする可能性があります。
| ケース | 入力 | 影響を受けるビルドでの効果 | トリガー |
|---|---|---|---|
| WebM/Vorbis 破棄ブリッジ | A_VORBIS トラックを持つ既存の WebM | テストされた両方の破棄モードにおいて、Chromium の AudioDiscardHelper リリースチェックでレンダラーが 0x80000003 (STATUS_BREAKPOINT) で終了する | 再生/デコード。メタデータの読み込みだけでは不十分 |
M4A 定数 stsz カウント | 既存のファストスタート AAC/M4A シード | メタデータの読み込み中に、レンダラーが約 6.6 GB (6.16 GiB) のプライベートメモリを一時的に割り当てる | audio 要素が preload="none" からメタデータに変わった後のメタデータ読み込み |
これらはサービス拒否/リソース消費のテストケースであり、実証されたコード実行エクスプロイトではありません。隔離された、境界のあるテスト環境でのみ実行してください。
media-gen は再現性のためのレイヤーであり、これらのケースがどのように発見されたかを示すものではありません。困難な部分は、大規模なネイティブ Discord 実行ファイルとその同梱メディアライブラリ内でこの挙動を見つけ出し、証明することでした。blare2 は、正確なランタイムの隔離されたコピーを書き換え、インストール済みアプリケーションを変更することなく狭い範囲のプローブで実行を観察できるようにすることで、それを実用的にしました。
WebM ケースでは、blare2 のセマンティックプローブが正確に AudioDiscardHelper::ProcessBuffers チェックで停止し、失敗状態を記録しました: discarded_frames = 129 および decoder_delay = 128。この観察がなければ、STATUS_BREAKPOINT によるレンダラー終了は、単に一般的なリリースアサーションを特定するだけであり、どのメディア不変条件が失敗したのか、あるいは細工されたパディングが実際にそこに到達したのかを立証することはできませんでした。
M4A ケースでは、大きな割り当ては一時的であり、通常のプロセスサンプルが取得される前に消えてしまう可能性があります。blare2 の正確なランタイムカバレッジと的を絞ったインストルメンテーションを、高頻度のプロセスサンプリングおよび逆アセンブリと組み合わせることで、小さなファイルが MOV サンプルテーブルビルダーに到達し、宣言されたカウントが AVIndexEntry とタイミングテーブルの割り当てをスケールさせたことが示されました。これにより、この問題は通常の AAC デコード失敗や、誤解を招くファイルサイズ/OOM の相関から切り離されました。広範な関数エントリカバレッジだけでは十分ではありませんでした。低カウントと高カウントのファイルは同じ関数をたどりましたが、その割り当てサイズは根本的に異なっていました。
blare2 はコンテナ解析、ソースレビュー、またはコントロールに取って代わるものではなく、Discord の本番アップロード/CDN パスがこれらのバイトを保持することを証明したわけでもありません。その価値はランタイムの帰属にありました。疑わしいパーサーの挙動を、正確な失敗状態と防御可能な割り当ての説明を伴う、再現可能でバージョン固定された発見に変えたのです。この種のバイナリインストルメンテーションがなければ、これら2つのケースの発見は大幅に遅くなり、検証ははるかに困難だったでしょう。
リポジトリのルートから:
cargo build --release -p media-gen
バイナリは target/release/media-gen (Windows では media-gen.exe) です。ジェネレーター自体に必要なのは Rust と Cargo.toml 内の依存関係だけです。
cargo run --release -p media-gen -- --help
cargo run --release -p media-gen -- webm --help
cargo run --release -p media-gen -- m4a --help
Matroska の負の DiscardPadding は Vorbis のフロントスキップになります。Chromium の旧来の Vorbis パスはそのメタデータを1つのエンコード済みパケット分遅延させます。現在の Chromium はそれを現在のデコード済み出力に適用し、そのプライミングパケットが PCM を出力しない場合、パケット0のメタデータを破棄します。1つの大きなスキップをパケット0と1にわたって繰り返すことで、2つの挙動を橋渡しします:
| オーディオパケット | デコード済み PCM | フロントスキップ |
|---|---|---|
| 0 | なし (Vorbis プライミング) | 577 フレーム |
| 1 | 576 フレーム | 577 フレーム |
トラックには 128 フレームの CodecDelay があります。旧来の遅延モードでは、パケット0の577フレームのスキップがパケット1に適用されます。現在のモードでは、パケット0のスキップは破棄され、パケット1の同一のスキップが直接適用されます。いずれの場合も、デコーダ遅延オフセット後に削除できるのは448フレームだけなので、129フレームがパケット2に持ち越されます。その正のフロントスキップは、これら129フレームがすでに削除された後に Chromium のリリースチェックに到達します。必要な不変条件は discarded_frames <= decoder_delay です。129 <= 128 は失敗し、レンダラーを終了させます。
ジェネレーターは Vorbis セットアップヘッダーを解析し、パケット1と2のデコード済みサイズを計算し、ブリッジが実行可能でなければフェイルクローズします。次に:
SimpleBlock 要素を BlockGroup 要素に変換します;DiscardPadding を、パケット2に正の1フレームトリガーを書き込みます;SeekHead、Cues、および影響を受けるクラスタ CRC 要素を置き換え、書き換えられたコンテナが解析可能なままであるようにします。マニフェストは即時パスと遅延パスを別々に報告します。チェックインされているサンプルでは、両方のパスがパケット2をチェックパケットとして指定し、expected_carry_frames: 129 および expected_check_fails: true を報告します。
リポジトリには 43 ms、4,185 バイトのコントロール WebM が含まれています:
cargo run --release -- webm \
--input samples/short-vorbis-dual-control.webm \
--output .build/short-vorbis-dual-crash.webm \
--manifest .build/short-vorbis-dual-crash.json \
--force
入力には以下が含まれている必要があります:
A_VORBIS トラック;CodecDelay (通常はトラックから読み取られます); そして便利なオプションは --trigger-skip、--codec-delay、および --sample-rate です。--second-skip は、名前が変更された --trigger-skip オプションのエイリアスのままです。デフォルトは、既知の失敗状態に必要な値です。このコマンドは JSON レポートを stdout に出力し、--manifest オプションが指定されている場合は同じレポートをそこに書き込みます。
チェックインされている候補は 43 ms、4,206 バイトです。その SHA-256 は:
a0ab9e146c629f037b86612addc1ab6fff9200711d45aa5f5edbb9576cc206ac
入力、パケットウィンドウ、またはオプションが変更されれば、出力ハッシュは変わることが予想されます。
M4A ケースは、エンコードされた AAC ペイロードではなく、有効な MP4 サンプルテーブルの形状を悪用します。ジェネレーターはシードを次のように変更します:
stsz サンプルサイズ配列を sample_size = 1 に置き換えます。stsz、最初の stsc ラン、および最初の stts ランで宣言されたサンプルカウントを同じ大きな値に設定します。stco オフセットを修復し、1バイトの AAC ペイロードが依然として mdat 内を指すようにします。固定された FFmpeg デマルチプレクサは、インデックスとタイミングテーブルを構築する際に、宣言されたカウントを権威あるものとして扱います。関連する構造体は、AVIndexEntry に宣言されたサンプルあたり約24バイト、タイミングデータにサンプルあたり12バイトを使用します: 合計でカウントエントリあたり36バイトです。テストされたビルドで受け入れられる最大カウントは 178,956,969 (0x0AAAAAA9) であり、これは次のように見積もられます:
178,956,969 * 24 = 4,294,967,256 bytes
178,956,969 * 12 = 2,147,483,628 bytes
combined = 6,442,450,884 bytes
正確な Discord レンダラーは、メタデータの読み込み中に 6,614,761,472 バイトのプライベートメモリピークに達しました。隣接するカウント 178,956,970 は、固定された FFmpeg の境界によって拒否され、通常のメモリ付近にとどまりました。これは制御されていないリソース消費 (CWE-400) であり、観測された整数ラップ、負のサイズ割り当て、または範囲外書き込みではありません。実際の AAC ペイロードは切り詰められていてもかまいません。大きな割り当てに到達するために再生が成功する必要はありません。
ジェネレーターは、AAC エンコーダーを埋め込む代わりに、意図的にシードを受け入れます。1つの明示的な stsz テーブル、1つの stsc ラン、1つ以上の stts エントリ、および stco チャンクオフセットを持つファストスタート AAC/M4A ファイルを使用してください。必要な構造が存在しない場合はフェイルクローズします。
cargo run --release -p media-gen -- m4a \
--input path/to/base-faststart.m4a \
--output .build/media-gen/constant-stsz.m4a \
--manifest .build/media-gen/constant-stsz.json \
--force
デフォルトの --sample-count は 178956969 で、固定されたビルドで受け入れられる最大値です。低メモリのスモークテストには、より小さな値を使用してください。例えば:
cargo run --release -p media-gen -- m4a \
--input path/to/base-faststart.m4a \
--output .build/media-gen/constant-stsz-32000000.m4a \
--sample-count 32000000 \
--force
--sample-count 178956970 は有用な隣接拒否コントロールであり、トリガー値ではありません。オプションの --moov-at-end フラグは moov を mdat の後に移動し、stco/co64 を更新します。これは、物理的な AAC バイトがメタデータより前に出現するファイルをテストする場合に有用ですが、割り当てメカニズムには必須ではありません。
そのレイアウトを明示的に生成するには:
cargo run --release -p media-gen -- m4a \
--input path/to/base-faststart.m4a \
--output .build/media-gen/constant-stsz-moov-end.m4a \
--moov-at-end \
--force
対応する成果物は
samples/short-vorbis-dual-control.webm、
samples/short-vorbis-dual-crash.webm、
および samples/short-vorbis-dual-crash.json です。
| 2 | 1,024 フレーム | 1 フレーム |