Chrome CVE-2026-85046 V8 の型混同を「マップ」から読む

Google Chrome の JavaScript エンジン V8 に型混同(type confusion)の脆弱性が見つかっている。CVE-2026-85046 である。CVSS 3.1 の基本値は 8.8(High)、修正版は 152.0.7977.82、CISA の KEV カタログには 2026年9月4日に収載され、是正期限は 2026年9月18日とされている。KEV に載っているということは、実際に攻撃で使われていることが確認されているという意味である。

NVD の記述はこうだ。「152.0.7977.82 より前の Google Chrome の V8 における型混同により、遠隔の攻撃者が細工した HTML ページ経由でサンドボックス内の任意コード実行を許した」。分類は CWE-843 である。

この記事では、型混同がなぜ JavaScript エンジンで起きるのかを V8 の「マップ」という仕組みから読み解き、最後に Node.js だけで、その前提と防波堤を実際に目で見るスクリプトを置く。Chrome をビルドする必要はない。

なお、この CVE そのものの再現手順は書かない。Chromium の課題番号 542403045 は現時点で閲覧に権限が要り、具体的な壊し方は公開されていない。ここで扱うのは型混同という欠陥の形であって、この CVE の攻撃コードではない。

そもそも「型混同」とは何か

型混同は、あるメモリ領域を、実際とは別の型のオブジェクトとして扱ってしまう誤りである。

C++ で書くと分かりやすい。

1
2
3
4
5
6
struct A { int32_t n; };
struct B { char* ptr; };

A a{ 0x41414141 };
B* b = reinterpret_cast<B*>(&a); // 本当は A なのに B として読む
puts(b->ptr); // 0x41414141 をポインタとして参照してしまう

a の中身はただの整数だが、B として読んだ瞬間、その整数はポインタとして解釈される。攻撃者が整数の値を決められるなら、参照先アドレスを決められることになる。ここから読み書きの原始的な手段(primitive)を組み立てていくのが、この種の欠陥の典型的な出発点だ。

問題は、JavaScript には型の宣言が無いのに、なぜエンジンの内部で型の取り違えが起きるのかである。答えは最適化にある。

V8 は「オブジェクトの形」に賭けて最適化する

V8 は、オブジェクトのプロパティ配置を Map(隠しクラス、hidden class とも呼ばれる)という内部オブジェクトで管理している。{ x: 1 }{ x: 2 } は同じ Map を共有し、{ x: 1, y: 2 } は別の Map になる。プロパティを足すと Map が遷移する。

次のような関数を考える。

1
function readX(o) { return o.x + 1; }

同じ形のオブジェクトばかりを何万回も渡すと、V8 の最適化コンパイラ TurboFan は「この o の Map は毎回これだ」という観測(型フィードバック)に賭けて、機械語を生成する。生成されたコードは、o.x を名前で探すのをやめ、「先頭から数えて何バイト目」という固定のオフセットを直接読むものになる。速いのは、探す処理が丸ごと消えているからだ。

ここで重要なのは、これが賭けだという点である。次に渡ってくるオブジェクトの形が違えば、そのオフセットの先には別の意味の値が入っている。整数のつもりで読んだ場所に、実はポインタが入っているかもしれない。それがまさに型混同である。

だから最適化コードは、本体に入る前に必ずガードを置く。「渡された o の Map は、賭けたときの Map と同じか」を確認し、違ったら最適化コードを捨てて元のバイトコード実行に戻す。これをデオプティマイズ(deoptimization)と呼ぶ。

型混同の脆弱性とは、このガードが抜けている、あるいは前提の立て方が間違っている状態を指す。ガードが正しく働いている限り、形の違うオブジェクトは安全な経路へ落ちる。

実際に見る: 賭けとガードを観測する

言葉だけだと信じにくいので、Node.js で観測する。Node.js は V8 を積んでいるので、Chrome と同じ仕組みがそのまま動く。

まずファイルを作る。

1
2
3
4
5
6
7
8
9
10
11
// shape.js
function readX(o) { return o.x + 1; }

// 形の同じオブジェクトだけで温める
const warm = { x: 1 };
for (let i = 0; i < 100000; i++) readX(warm);
console.log('warmup done');

// 形の違うオブジェクトを1つ渡す
const other = { y: 9, x: 1 };
console.log('result =', readX(other));

warm{x} の形、other{y, x} の形である。プロパティの並び順が違うので、Map は別物になる

デオプティマイズの記録を出すフラグを付けて実行する。

1
node --trace-deopt shape.js

手元の Node.js v20.17.0(V8 11.3.244.8)での出力から、関係する行だけ抜き出すとこうなる。

1
2
3
4
5
warmup done
[bailout (kind: deopt-eager, reason: wrong map): begin. deoptimizing
0x... <JSFunction readX (sfi = 0x...)>, 0x... <Code TURBOFAN>, opt id 0,
bytecode offset 0, deopt exit 1, ...]
result = 2

読むべきは3点ある。

  1. <Code TURBOFAN> — 温めた結果、readX は実際に最適化コンパイラの機械語になっていた。
  2. reason: wrong map — 形の違うオブジェクトが来たことを、Map の一致確認で検出している。理由の文字列がそのまま「マップが違う」と言っている。
  3. result = 2 — それでも計算結果は正しい。ガードが働いて安全な経路に落ち、o.x を通常のやり方で読み直しているからだ。

この wrong map こそが防波堤である。ここが無い、あるいは確認する Map の選び方を間違えていると、{y, x} のオブジェクトを {x} の配置だと思って読むことになり、o.x の位置には y の値が入っている。数値だけならまだ計算がおかしくなるだけだが、片方がポインタを持つ型なら、整数がポインタとして解釈される。C++ の例と同じ状態になる。

最適化されたこと自体を確かめたい場合は次でも見られる。

1
node --trace-opt shape.js 2>&1 | grep -i "optimizing readX"

より踏み込んで最適化状態のビットを見る方法もある。

1
2
3
4
5
6
7
8
node --allow-natives-syntax -e '
function add(o){ return o.x + 1; }
const a = { x: 1 };
for (let i = 0; i < 20000; i++) add(a);
console.log("warm :", %GetOptimizationStatus(add).toString(2));
add({ x: 1, y: 2 });
console.log("after :", %GetOptimizationStatus(add).toString(2));
'

%GetOptimizationStatus は V8 の内部関数で、--allow-natives-syntax を付けたときだけ使える。手元では、形を変えた後にビット列が変化する(最適化コードが外れる)ことが確認できた。ビットの意味は V8 のバージョンによって変わるので、値そのものを覚えないこと。 変化したかどうかだけを見る用途に留めるのが安全である。この 4 行は調査用で、本番のコードに入れるものではない。

「サンドボックス内」という限定の意味

NVD の記述にある「サンドボックス内の任意コード実行」は、軽く見てよいという意味ではないが、正確に読む必要がある。

Chrome は、ウェブページを描画するレンダラプロセスを、OS から見て強く制限された権限で動かしている。V8 の欠陥だけで奪えるのは、この制限された箱の中である。端末そのものを取るには、箱から出るための別の欠陥(サンドボックス脱出)を組み合わせる必要がある。

とはいえ、箱の中に入られた時点で、そのタブが扱っている内容は攻撃者のものになりうる。そして CISA が KEV に載せているということは、現実の攻撃で既に使われているということである。「単体では端末を取れない」は、対応を先延ばしにしてよい理由にはならない。

やること

対応は1つだけである。Chrome を 152.0.7977.82 以降へ更新して、ブラウザを再起動する。

1
chrome://settings/help

をアドレスバーに入力すると、実行中のバージョンが出て、必要なら更新が始まる。更新は再起動するまで適用されない。 Chrome の自動更新は既定で有効だが、何日もタブを開いたままの端末では古いバージョンが動き続ける。組織で管理しているなら、配布設定ではなく「実際に動いているバージョン」を資産管理や MDM の側で集計すること。配布は正しいのに再起動されていない端末が残る、というのが実務上の典型的な穴だ。KEV の是正期限は 2026年9月18日である。

見落としやすいのはサーバ側である。CI やスクレイピングで headless Chrome / Puppeteer / Playwright を動かしているコンテナは、自動更新の対象外で、ベースイメージを作り直すまで古い V8 を積んだままになる。ブラウザの CVE を「利用者端末の話」として仕分けると、この経路が丸ごと抜ける。

Chromium を組み込んだ他の製品も同じエンジンを持つが、それぞれ独自にビルドしているため、該当性と修正版は製品ごとの告知で確認する必要がある。ここでバージョンを推測して書かないこと。当サイトでもこの記事を書いた時点(2026-09-05)で、Chrome 以外の製品については一次情報を確認していない。

まとめ

  • 型混同は、あるメモリ領域を別の型として扱う誤りで、整数がポインタとして解釈されうる
  • JavaScript に型宣言が無くても起きるのは、V8 がオブジェクトの形(Map)に賭けて最適化するから
  • 賭けが外れたときに戻す仕組み(デオプティマイズ)が防波堤で、--trace-deoptreason: wrong map として実際に観測できる
  • CVE-2026-85046 は KEV 収載済み(2026-09-04・是正期限 2026-09-18)。対応は 152.0.7977.82 以降への更新と再起動
  • CI の headless Chrome は自動更新されない。イメージを作り直す

掲載中の CVE と各クラウドでの対応状況は CVE Watch にまとめている。