V8 ゼロデイ CVE-2026-85046 を読み解く — sort の最適化が生んだ型混同と Electron への波及

はじめに

9 月 3 日、Google が Chrome の緊急アップデートを出した。CVE-2026-85046、CVSS 8.8、V8 の型混同(type confusion)。すでに実環境で悪用されていることが Google 自身のアドバイザリで確認されている、2026 年 6 件目のゼロデイだ。CISA は翌 4 日に KEV カタログへ追加し、連邦機関に 9 月 18 日までの修正を求めている。

Hacker News では「Actively Exploited Sandbox RCE in Chromium」というタイトルで 698pt を集めた。ただしこの表現には注意が必要で、これはサンドボックス脱出ではない。V8 のアドバイザリが言っているのは「サンドボックスの内側で任意コード実行に至る」であって、OS まで抜ける話ではない。

では騒ぐほどではないのか、というと逆で、サンドボックスの内側で止まるかどうかは実行環境が決める。Chrome なら止まる。Electron や WebView に組み込まれた Chromium では、止まる保証は構成次第で変わる。

この記事では、発見者本人が公開したライトアップをもとに、(1) このバグが V8 のどこにあり何を壊したのか、(2) 型の取り違えがなぜ任意読み書きになるのか、(3) 「サンドボックス内 RCE」という但し書きが Chromium 派生プロダクトでどう意味を変えるのか、を順に整理する。

何が起きたのか

事実関係を先に置く。

  • 報告: 2026 年 8 月 4 日、セキュリティ研究者 Salvatore Gulizia(Serotav)による報告。賞金は $1,000
  • 修正: 2026 年 9 月 3 日の Stable Channel アップデート。全 12 件の脆弱性を含む
  • 修正版: Windows / macOS が 152.0.7977.82 および .83、Linux が 152.0.7977.82
  • 悪用状況: 公開時点で「exploit が実環境に存在する」と Google が明記。攻撃者・標的・キャンペーンの詳細は非公開
  • KEV 追加: 2026 年 9 月 4 日

同時に修正された残り 11 件も軽くはない。CrashReporting、Network、Compositing、V8 の別のレースコンディション、WebGL、CacheStorage、DevTools、Skia に高深刻度の問題が含まれている。悪用されているのは CVE-2026-85046 だけだが、更新をかける理由は 1 件ではない。

なお報告から修正まで約 1 ヶ月ある。この期間に「実環境の exploit」が観測されているということは、報告者とは独立に同じバグ(あるいは同じ攻撃面)へ到達していた第三者がいたことを意味する。V8 の最適化コンパイラは、バグバウンティハンターと攻撃者が同じ場所を掘り続けている領域だ。

ElementsKind — 同じビット列が別の意味になる仕組み

このバグを理解するには、V8 が配列をどう表現しているかを押さえる必要がある。

V8 は JavaScript の配列を、中身の型に応じて ElementsKind という内部分類で最適化する。主要なものは 3 つだ。

  • PACKED_SMI_ELEMENTS — 小さな整数(Smi)だけの配列。値はタグ付き整数としてそのまま格納される
  • PACKED_DOUBLE_ELEMENTS — 浮動小数点数が入った配列。生の double として格納される
  • PACKED_ELEMENTS — 任意のオブジェクトが入りうる配列。値はタグ付きポインタとして格納される

[1, 2, 3] は SMI、そこに 1.5 を入れると DOUBLE へ、{} を入れると PACKED_ELEMENTS へと遷移する。V8 の公式ブログが説明しているとおり、この遷移は格子(lattice)を下る一方向で、後から float を Smi で上書きしても DOUBLE のまま戻らない。一度緩い側へ移ったら戻らない、というのが原則だ。

この原則に例外があることが、今回のバグの前提になっている。 後述する Array.prototype.fill() は、全要素を置き換えるケースに限り「現在の中身を無視して ElementsKind チェーンを逆向きに辿り、Map を巻き戻す」最適化を持っている。全部上書きするなら古い中身の型を気にする必要はない、という理屈で、単体では正しい。

重要なのは、メモリ上の同じビット列が、ElementsKind によって完全に別の意味を持つという点だ。ある 64 bit の値を、SMI だと思って読めば「整数」、PACKED_ELEMENTS だと思って読めば「ヒープ上のオブジェクトへのポインタ」になる。JIT コンパイラはこの前提を信じて、境界チェックや型チェックを削り落とした機械語を吐く。だから前提が崩れた瞬間、削られたチェックの分だけそのまま攻撃者の手に落ちる。

CVE-2026-85046 は、まさにこの**「PACKED_ELEMENTS の中身を持つ配列に PACKED_SMI_ELEMENTS の Map を被せる」**ことに成功したバグだ。

バグの正体:Maglev の sort インライン化とマップ検査の穴

バグがあったのは Maglev — Chrome の中位層 JIT コンパイラ(インタプリタの Ignition と最上位の TurboFan の間に位置する)の maglev-graph-builder.cc にある TryReduceArrayPrototypeSort だ。

この関数は名前のとおり、Array.prototype.sort() の呼び出しをビルトインへのジャンプではなく、制御フローグラフに直接インライン展開した挿入ソートに置き換える最適化を行う。ソートは呼び出し頻度が高く、ビルトイン境界を跨ぐコストが目立つので、投機的にインライン化する価値がある。

問題は、この最適化が比較関数(コンパレータ)を呼んだ後の検証にあった。sort(compare) のコンパレータは任意の JavaScript であり、その中で配列そのものを触れてしまう。だから最適化コードはコンパレータ実行後に「レシーバの Map がまだ想定どおりか」を確認する必要がある。

ライトアップはここを的確に指摘している。

The code doesn’t check if the map changed, it simply checks that the current array map is any of the maps in receiver_maps_before_loop.

つまり検査が「Map が変わっていないこと」ではなく「Map が事前に観測した集合のいずれかであること」になっていた。そして最適化のトレーニング段階で PACKED_SMI_ELEMENTS と PACKED_ELEMENTS の両方を食わせておけば、この集合には両方が入る。片方から片方へ遷移しても検査を通ってしまう。

発見者が公開しているトリガーは驚くほど短い。

function confuse(arr){
    function compare(){
        arr.fill(0);   // 全要素置換 → Map を PACKED_SMI_ELEMENTS へ巻き戻す
        return 0
    }
    arr.sort(compare);
}

// 2 つの ElementsKind を両方観測させる
for (let i = 0; i < 1000; i++){
    confuse([6,9,4,2,0])   // PACKED_SMI_ELEMENTS
    confuse([{},{},{}])    // PACKED_ELEMENTS
}

ここで先ほどの fill() の例外が効く。Maglev はソート中、作業用の一時配列を作って要素をコピーする。状態を追うとこうなる。

  1. コンパレータ実行前 — レシーバの Map は PACKED_ELEMENTS、一時配列にはオブジェクトポインタが入っている
  2. コンパレータ内 — arr.fill(0) が全要素を Smi の 0 で置き換え、レシーバの Map を PACKED_SMI_ELEMENTS へ巻き戻す。この時点でレシーバの中身は [0, 0] なので、Map と中身は整合している
  3. ソート後のコピーバック — 一時配列が保持していたオブジェクトポインタがレシーバへ書き戻される。しかし Map は PACKED_SMI_ELEMENTS のまま

結果、中身はポインタ、Map は「Smi しか入っていない」と主張している配列ができあがる。緩い Map 検査がこの状態を素通しした瞬間に型混同が成立する。fill() の巻き戻し最適化も、sort のインライン化も、それぞれ単体では正しい。両者が同じ配列に対して同時に成立したときだけ破綻する、という組み合わせのバグだ。

同じ欠陥は Maglev だけでなく TurboFan 側にも存在していたと報告されている。修正は、ElementsKind が混在するケースでインラインソート最適化を無効化する形で入った(導入コミット 66a3f1e9、修正コミット e0562d87)。

型混同から任意読み書きへ

型混同それ自体は「配列の中身が変な値として読める」までで、そこから任意読み書きに持ち上げるには追加の道具が要る。ライトアップはその過程も示している。

addrof プリミティブ(オブジェクトのアドレスを数値として取り出す)は、混同状態の配列を文字列化するだけで得られる。

function addrof(target) {
  let sacrificial = [target,target];
  confuse(sacrificial);
  return (Number(String(sacrificial).split(',')[0]) << 1) | 1
}

スロットの中身は実際にはポインタだが、SMI として解釈されているので String() が素直に数値として出力してしまう。ヒープ上の配置がここで漏れる。

任意書き込み側はもう少し込み入っていて、Array.prototype.unshift() が使われている。unshift が再確保なしで要素をずらす場合、Maglev が生成したコードはそれを「SMI から SMI への移動」とみなし、ライトバリアの発行を省略する。ライトバリアは GC がオブジェクト間の参照を追跡するための仕組みなので、これを迂回してタグ付きポインタを書き込めるということは、GC の安全性の前提を崩せるということだ。あとは混同配列と GC の昇格タイミングを組み合わせて偽オブジェクトを構築すれば、完全な読み書きプリミティブになる。

この流れは V8 exploitation の教科書的な形をしている——型情報の取り違え → addrof → 偽オブジェクト → 任意読み書き。JIT コンパイラのバグが一貫して高値で取引される理由でもある。「配列の Map の検査が緩い」という一点から、レンダラプロセスのメモリ全域が取れてしまう。

「サンドボックス内の RCE」はどこまで安心なのか

ここが実務上いちばん重要な部分だ。

Chrome では、レンダラプロセスは OS レベルのサンドボックスに閉じ込められている。V8 の任意コード実行に到達しても、そこからファイルシステムやプロセス生成へ抜けるには別の脆弱性(サンドボックス脱出や権限昇格)が必要になる。だから「サンドボックス内 RCE」は「即座に端末が落ちる」ではない。実際、発見者自身も V8 CTF では別の n-day サンドボックス脱出と連鎖させてフル制御を達成している。単体では足りない、という証拠でもある。

ただし、これはサンドボックスが実際に効いている場合の話だ。Chromium は Chrome の外にも広く埋め込まれている。

  • Chromium 系ブラウザ — Edge、Brave、Opera、Vivaldi。V8 は Chromium コードベース共通なので、同じ欠陥を継承する。各ベンダの更新を待つ必要があり、Chrome より数日遅れる
  • Electron アプリ — リモートコンテンツを読み込むアプリは、更新済み Chromium を同梱するまでリスクを引き継ぐ
  • CEF / WebView 組み込みアプリ — 埋め込み側のアップデート経路に依存する
  • Android System WebView — Play ストア経由の更新なので、端末側の更新状況に左右される

Electron については構成の影響が明確に出る。Electron 20 以降、レンダラプロセスのサンドボックスはデフォルトで有効になった。sandbox: true が効いていれば、V8 の exploit はサンドボックス化されたレンダラの内側に留まり、影響は Chrome と同程度に抑えられる。逆に sandbox: false にしている(あるいは古い Electron を使っている)と、レンダラはフルのシステム権限で動くので、V8 のメモリ破壊がそのまま RCE になる。

さらに、サンドボックスが有効でも preload スクリプトの API を経由してメインプロセス側の高権限機能に触れる経路が残りうる。Electron のセキュリティドキュメントが「リモートコンテンツを読み込むなら sandbox の有無に関わらず対策が要る」と書いているのはこのためだ。「V8 の脆弱性はサンドボックス内で止まる」は、Chrome の設計を借りているだけで、自分のアプリの設計がそれを保証しているとは限らない。

Electron のバージョン対応も確認しておく価値がある。Chromium 152(M152)を載せているのは Electron 44.0.0(2026 年 8 月 25 日 stable)だ。Electron の Chromium 更新は「新しい安定版 Chromium リリースの 1〜2 週間後」が目安とされているが、これは保証ではない。サポート対象は最新 3 メジャーバージョンなので、Electron 42 系(M148)を使っているアプリは、そもそもサポート期限(2026 年 10 月 20 日)が近い。

エンジニアが今日やること

優先順に整理する。

1. ブラウザ本体 Chrome は 152.0.7977.82 以上か確認する。ヘルプ → Google Chrome について で確認し、再起動まで完了させる。自動ロールアウトを待つのではなく、明示的に確認したほうがいい(ダウンロード済みでも再起動しないと切り替わらない)。Edge / Brave / Opera / Vivaldi は各ベンダの更新を適用する。

2. Electron アプリ 自社で配布しているなら、同梱 Chromium のバージョンを確認し、パッチ済みビルドが出ているメジャーへ上げる。あわせて sandbox 設定、contextIsolation、nodeIntegration、リモートコンテンツを読み込む箇所を棚卸ししておくと、次の V8 ゼロデイのときの判断が速くなる。V8 のメモリ破壊バグは Chromium のリリースごとに何件も修正されている——今回だけの話ではない。

3. モバイルの WebView アプリ内ブラウザや WebView で外部コンテンツを表示している箇所は、Android System WebView / WKWebView の更新状況が防御ラインになる。特に JavaScript ブリッジでネイティブ機能を露出している箇所は、レンダラ側の任意コード実行が直接ネイティブ API に届く経路になりうる。ここは「サンドボックス内で止まる」の前提が最も崩れやすい。

4. CI / 自動化環境 Playwright、Puppeteer、ヘッドレス Chrome で外部の URL を開いているパイプラインがあるなら、そこも攻撃面だ。クローラやスクリーンショット生成のように、信頼できないページを自動で開く構成は特に該当する。

5. 検知 すでに踏んでいないかを見るなら、レンダラのクラッシュに続く異常なプロセス活動、想定外の子プロセス生成、メモリ破壊を示す挙動、そして疑わしいドメインへのアクセス直後の異常を突き合わせる。連鎖に必要な「別のサンドボックス脱出」を試みた痕跡も同じ文脈で見る。

まとめ

  • CVE-2026-85046 は V8 の Maglev(および TurboFan)にあった型混同で、Array.prototype.sort のインライン化がコンパレータ実行後の Map 検査を「変化していないこと」ではなく「既知の集合に含まれること」で済ませていたのが原因
  • 鍵は Array.prototype.fill() の「全要素置換なら ElementsKind を巻き戻せる」最適化。ElementsKind の遷移は原則一方向だが、この例外と sort のインライン化が噛み合ったときだけ PACKED_ELEMENTS → PACKED_SMI_ELEMENTS の取り違えが起きる
  • そこから addrof とライトバリア迂回を経て JS ヒープの任意読み書きに到達する
  • これはサンドボックス脱出ではない。単体で OS を取るには別の脆弱性との連鎖が要る
  • ただし「サンドボックス内で止まる」は Chrome の設計が保証しているだけで、Electron の設定次第、WebView のブリッジ設計次第で境界の意味は変わる
  • 修正版は Chrome 152.0.7977.82 以上。CISA KEV 入り済みで、実環境の悪用が確認されている

JIT コンパイラの投機的最適化は、「この配列はずっと整数だけを持つ」という前提でチェックを削ることで速度を得ている。その前提を崩す方法が 1 つ見つかれば、削られたチェックの数だけ攻撃者に余地が生まれる。V8 のゼロデイが毎年繰り返し出るのはこの構造によるもので、今後も「更新を早く当てる経路が自分のプロダクトにあるか」が実質的な防御力になる。Electron や WebView を抱えているチームは、そのリードタイムを一度測っておくといい。

今日のその他のニュース

Firebase AI Logic が gemini-3.7-flash に対応 / Flutter SDK v4.19.0 — Gemini Developer API 経由なら Blaze プラン不要で最新安定モデルを利用できる。あわせて App Check のアテステーションプロバイダとして reCAPTCHA Enterprise が Preview 提供開始(Apple / Android / Flutter 対象)。(Firebase release notes)

Claude Code の Rules が機能しない事象が話題 — はてブ 225。同日に「ルールではなく Skill に指示を書く」「CLAUDE.md を Claude 5 世代向けに見直す」という記事が並んでおり、グローバルなルールから Skill 単位の指示へという揺り戻しが同時多発している。(kawasin73.hatenablog.com)

N+1 を潰そうとして巨大 JOIN でメモリを食い潰した話 — 「N+1 を見つけたら JOIN」という定石を、原因診断を飛ばして機械的に適用した結果の失敗記録。症状に対する定石の処置が、前提確認なしに適用されたときに壊れる典型例。(Qiita)

ソース