ガードされていない設定項目: Chrome における GTK3 Gtk/Modules XSETTING 経由の GPU→ブラウザ サンドボックス脱出
この記事では、私が Chrome VRP に報告した Linux/X11 向けのサンドボックス脱出脆弱性について解説します。端的に言うと、侵害された GPU プロセスが Gtk/Modules という 1 つの XSETTING を通じて、Chrome のサンドボックス外のブラウザプロセス内で動作する GTK3 に、実行可能 memfd にステージングした攻撃者制御の ELF をロード・初期化させることができる、というものです。
Ozone/X11 上の Chrome は、GPU プロセスの X11 接続を GPU seccomp-BPF サンドボックスのインストール前に意図的に作成します(後から開こうとすると socket()/connect() がブロックされるため)。侵害された GPU プロセスは、この保持された認証済み接続を使って XSETTINGS manager となり、Gtk/Modules=/proc/<gpu-pid>/fd/<fd> を公開できます。ブラウザプロセス内の GTK3 は、その wire 名を gtk-modules プロパティに対応づけ、値を Chrome の set_property interceptor を介さずにコピーし、絶対パスを受け入れて g_module_open() を呼び、gtk_module_init を解決して実行します。つまりブラウザプロセスでのネイティブコード実行です。
この問題で注目すべき点をいくつか挙げます:
- Chrome は同一の「侵害された GPU」脅威モデルに基づく XSETTINGS 悪用をすでに 3 件修正していました(カーソルテーマ、アイコンテーマ、テーマ/キー名)。その一方で、値がネイティブコードのパスそのものになる唯一のプロパティ
gtk-modulesだけが無防備なまま残っていました。 - 既存の interceptor を拡張するだけでは不十分だった点も興味深い点です。GTK3 の動的 XSETTING 経路は、Chrome がインターポーズする GObject の
set_propertyvtable を呼ばずに private property storage を直接更新します。 - 最初の修正(プロパティを空の application-owned 値に pin)は報告から 2 日で main に入りましたが、その後
gtk_init_check()内で消費される初期 XSETTINGS バッチ経由で bypass 可能であることが判明し、interceptor を GTK 初期化より前に移す第 2 の修正が必要になりました。この後処理の最中に、第 1 修正のブランチ cherry-pick は revert される流れになりました。
背景
X11 における GPU/browser 境界
Chrome の Linux プロセスモデルでは、GPU プロセスはブラウザプロセスより低い信頼度として扱われ、seccomp-BPF ポリシー下で動作します。セキュリティ要件は「GPU プロセス内での侵害が、サンドボックス外のブラウザプロセスでのネイティブコード実行に変換されないこと」です。
しかし Ozone/X11 では、グラフィックス初期化のために生存した X11 接続が必要です。そのため Chromium は GPU サンドボックスをインストールする前にこの接続を作成します。ui/ozone/platform/x11/ozone_platform_x11.cc の OzonePlatformX11::InitializeGPU() は、この順序を明示しています:
// Set up the X11 connection before the sandbox gets set up. This cannot be
// done later since opening the connection requires socket() and connect().
auto connection = x11::Connection::Get()->Clone();
connection->DetachFromSequence();
surface_factory_ozone_ =
std::make_unique<X11SurfaceFactory>(std::move(connection));
この接続は必要なものですが、同時に権限のチャネルでもあります。XSETTINGS プロトコルでは、任意の X11 クライアントが画面ごとの _XSETTINGS_S<n> selection を獲得し、owner window に _XSETTINGS_SETTINGS プロパティを設定して、MANAGER クライアントメッセージでその owner を告知できます。同じディスプレイ上の GTK アプリケーションは、選択された manager が公開する内容をそのまま消費します。
Gtk/Modules は実行可能な設定である
GTK3 の X11 設定マップは、wire レベルの Gtk/Modules という名前を GObject プロパティ gtk-modules に変換します(gdk/x11/gdksettings.c):
{"Gtk/KeyThemeName", "gtk-key-theme-name"},
{"Gtk/Modules", "gtk-modules"},
{"Gtk/ButtonImages", "gtk-button-images"},
これは装飾的な設定ではありません。空でない gtk-modules の値は 1 つ以上のネイティブ GTK モジュールを指し、GTK は各モジュールをロードして、エクスポートされた gtk_module_init エントリポイントを GTK をホストするプロセス内で呼び出します。Chrome の X11 構成では、そのホストはサンドボックスのないブラウザプロセスです。
既存の防御はテーマ系プロパティのみをカバー
Chromium は侵害された GPU プロセスが pre-sandbox の X11 接続を悪用しうることをすでに認識しています。GtkUi::Initialize() はデフォルトの GtkSettings を取得し、テーマ関連の値をサニタイズして、GtkSettings::set_property interceptor をインストールします。ui/gtk/gtk_util.cc の interceptor が扱うのは次の項目だけでした:
if (prop_name == "gtk-theme-name") {
property = ThemeProperty::kThemeName;
} else if (prop_name == "gtk-icon-theme-name") {
property = ThemeProperty::kIconThemeName;
} else if (prop_name == "gtk-key-theme-name") {
property = ThemeProperty::kKeyThemeName;
}
gtk-modules に対応するポリシーは存在しませんでした。
調査の経緯
調査の起点となったのは、「侵害された GPU / XSETTINGS」という脅威モデルを明示的に確立した一連の公開済み Chromium 修正です:
- コミット
633de441a4c8/ issue 518105731 — 侵害された GPU が XSETTINGS を乗っ取りうるため、カーソルテーマ名を検証 - コミット
48359cc8a62c/ issue 519258799 — ブラウザプロセスの画像パーサを攻撃者制御のパスに誘導されるのを防ぐため、アイコンテーマ名を検証 - コミット
89fe26fe4c58/ issue 519731111 — pre-sandbox GPU X11 接続が生存している間の、GTK テーマ/アイコン/キーテーマの write-time interceptor を導入
これらの変更がサニタイズするのはパースされるデータ、つまりテーマエンジンや画像デコーダに渡される名前です。XSETTINGS→プロパティの対応表をこれらと突き合わせたところ、実行コードを直接選択する唯一のプロパティ Gtk/Modules が、どの防御にもカバーされていないことが分かりました。
脆弱な経路は実際のランタイムに対してエンドツーエンドで確認しました。対象は未改変の Chrome for Testing Beta 152.0.7977.30 バイナリと AlmaLinux gtk3-3.24.43-5.el10.x86_64 パッケージで、後者はソース RPM を取得し、ロード経路上の 3 ファイル(gdksettings.c, gtksettings.c, gtkmodules.c)が調査対象の upstream GTK 3.24.43 tarball とバイト一致することを確認しています。同じキャンペーンの初期バージョンでは、公式 Chrome 150.0.7871.181 でもシンクを再現済みです。
脆弱性の詳細
1. 侵害された GPU は制御チャネルを保持している
脅威モデルは、Chrome の BPF サンドボックス化された GPU プロセス内でのネイティブコード実行を起点とします。保持された pre-sandbox X11 接続により、そのプロセスは _XSETTINGS_S0 を獲得して整形式の XSETTINGS ペイロードを公開できます。新しいソケットも、異常な X11 パケットも不要です。
| オブジェクト | プロセス | 攻撃者の制御 |
|---|---|---|
| XSETTINGS owner window | GPU | 完全 |
_XSETTINGS_SETTINGS のバイト列 | GPU | 完全 |
| 設定名 | GPU | Gtk/Modules |
| 設定値 | GPU | 任意の文字列 |
GtkSettings の消費者 | Browser | 特権シンク |
2. GPU ポリシーはファイルレスな実行可能ペイロードを許可する
ペイロードはディスク上の永続ファイルを必要としません。Chromium の GPU BPF ポリシーは prctl を許可し、memfd_create を実行可能マッピング版の memfd 制限に通します(sandbox/policy/linux/bpf_gpu_policy_linux.cc):
case __NR_prctl:
return Allow();
// ...
case __NR_memfd_create:
return RestrictMemfdCreateWithExecMappings();
RestrictMemfdCreateWithExecMappings() は MFD_EXEC を明示的に許可しています(sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc)。したがって攻撃者の手順は次の通りです:
memfd_create("name", MFD_ALLOW_SEALING | MFD_EXEC)を作成し、位置独立な GTK モジュールをそのディスクリプタに書き込むprctl(PR_SET_DUMPABLE, 1)を呼び、同一 UID のブラウザが/proc/<gpu-pid>/fd/を辿れるようにするGtk/Modules=/proc/<gpu-pid>/fd/<fd>を公開し、ブラウザの UI スレッドがMANAGERイベントを処理する間、owner window とディスクリプタを維持する
3. GTK は Chrome の interceptor を介さずに動的な値をコピーする
GTK が XSETTINGS 変更を受信すると、_gtk_settings_handle_event() がプロパティを検索し settings_update_xsetting() を呼びます。文字列値のプロパティについて、この関数は wire 上の値を取得し、GtkSettingsPrivate::property_values に直接コピーします:
if (!gdk_screen_get_setting (priv->screen, pspec->name, &val))
return FALSE;
g_param_value_validate (pspec, &val);
g_value_copy (&val, &priv->property_values[pspec->param_id - 1].value);
priv->property_values[pspec->param_id - 1].source =
GTK_SETTINGS_SOURCE_XSETTING;
これが決定的な遷移です。この経路は、Chromium がインターポーズする GObject の set_property vtable を呼びません。したがって GtkSettingsSetProperty() に gtk-modules の分岐を追加するだけでは、動的 XSETTING 経路は開いたままだったはずです。
続くクラス通知はすでに特権的な消費者です。gtk_settings_notify() は PROP_MODULES を settings_update_modules() の呼び出しで処理し、攻撃者の文字列を _gtk_modules_settings_changed() へ転送します。アプリケーションレベルの notify::gtk-modules コールバックでサニタイズするには遅すぎます。
4. GTK は proc-fd パスを受け入れて攻撃者のコードを呼ぶ
GTK のモジュールリゾルバは絶対パスを意図的に受け入れます:
if (g_path_is_absolute (name))
return g_strdup (name);
ローダーはそのパスを開き、モジュール初期化関数を呼び出します:
module = g_module_open (module_name,
G_MODULE_BIND_LOCAL | G_MODULE_BIND_LAZY);
// ...
else if (g_module_symbol (module, "gtk_module_init", &modinit_func_ptr))
modinit_func = modinit_func_ptr;
// ...
(* info->init_func) (>k_argc, >k_argv);
この時点でブラウザプロセスは、攻撃者制御の実行可能バイトをマップし、ネイティブな制御をそれに移譲しています。シンボライズすべきメモリ破壊クラッシュは存在しません。セキュリティ上の失敗は、意図されたモジュールロード経路が、より低い権限の侵害済みプロセス由来の入力で到達された、という点にあります。
破られた不変条件は次の通りです:
侵害された GPU プロセスが制御するデータが、サンドボックス外のブラウザプロセスにロード・実行されるコードを選択してはならない。
検証
これはメモリ破壊ではなくロジックベースのネイティブモジュールロード経路であるため、検証はクラッシュではなくプロセス同一性と実行可能マッピングの判別子に重点を置きました。
未改変の製品シンク: A/B/R キャンペーン
未改変の Chrome for Testing Beta 152.0.7977.30 バイナリ(SHA-256 d044c928…6e79aa)に対し、毎回新規の Xvfb ディスプレイとプロファイル、通常のサンドボックス設定で実施しました。LD_PRELOAD、--no-sandbox、--disable-gpu-sandbox は不使用です:
| Axis | Browser PID | Publisher PID | Marker PID | 実行可能ターゲットマッピング | 結果 |
|---|---|---|---|---|---|
A — Gtk/Modules memfd を公開 | 630499 | 630684 | 630499 | memfd inode 126305, r-xp | pass |
| B — 公開なし | 630735 | なし | なし | なし | pass |
| R — 新規リピート | 631011 | 631229 | 631011 | memfd inode 127235, r-xp | pass |
正の 2 軸では、無害なモジュールのマーカー PID がブラウザ PID と一致し、publisher PID とは異なりました。また、ステージングした memfd の inode がブラウザ内に r-xp でマップされており、外部 publisher への帰属や、パスのみ・古いファイルによる説明を排除しています。外部 publisher は、侵害された GPU が行うことの X11 プロトコル上のアナログであり、未改変製品上でブラウザ/GTK シンクをエンドツーエンドに証明するものです。
実 GPU のフィルタ後判別子
別の 3 ランキャンペーンでは、Chrome の実際の GPU PID に LD_PRELOAD プローブを配置し、Chrome 自身の seccomp(SECCOMP_SET_MODE_FILTER, ...) 呼び出しを挟み込みました。--gpu-sandbox-start-early 下では seccomp が Ozone/X11 初期化より前にインストールされるため、プローブはフィルタ前に専用の X11 接続とモジュールソース FD を開きます。各ランでは、実際のフィルタ済み GPU PID(NoNewPrivs: 1、Seccomp: 2、フィルタ 1 本)が X11 ソケットを保持し、フィルタインストール後に MFD_EXEC memfd を作成し、Gtk/Modules を公開して、ブラウザ PID での gtk_module_init とブラウザの実行可能マッピングを引き起こしました。
このキャンペーンは意図的に開示された計装を使用しています。証明するのは、Chrome の実際の GPU ポリシー下でのフィルタ後 syscall および保持ディスクリプタの能力であり、完全な end-to-end exploit や Web/renderer トリガーを主張するものではありません。
正直なネガティブ記録として、製品が生成した XCB 接続をそのまま再利用しようとする試みは、Xvfb/ソフトウェアレンダリングのテストホストでは目標状態に到達せず(dri3 extension not supported)、マーカーも生成しませんでした。これは環境由来のギャップであり、プリミティブの肯定・否定どちらの証拠でもありません。結果を強引に出すためにサンドボックスを弱めることはしていません。
修正候補の差分検証
提出前に、提案する修正を正確な製品上でも検証しました。同一の Chromium タグ(cbd60d12ee30)から同じ GN 引数で 2 つのバイナリをビルドし、差異は gtk-modules を空の application-owned 値に pin する候補パッチの有無のみとしました。
| Axis | XSETTINGS 公開 | Browser マーカー | ターゲットモジュールマップ | 結果 |
|---|---|---|---|---|
| N — パッチ適用 | 成功 | なし | なし | pass |
| R — クリーン復元 | 成功 | あり(正確なブラウザ PID) | あり(r-xp) | pass |
パッチ適用ビルドでも攻撃者の公開自体は成功しますが、その設定はコードロードに変換されなくなりました。パッチを外すと両方の結果が復帰します。
修正と展開の経緯
この issue は に報告され、同日中に Chromium の担当者に割り当てられました。、CL 8243490 が main にマージされました。提案した最小限の修正そのものです:
GtkSettings* settings = gtk_settings_get_default();
// Pin `gtk-modules` to an empty string with APPLICATION source priority
// to prevent XSETTINGS updates from loading GTK modules.
g_object_set(settings, "gtk-modules", "", nullptr);
GTK3 は g_object_set() で書かれた値を GTK_SETTINGS_SOURCE_APPLICATION の優先度で記録し、settings_update_xsetting() は application-sourced の値を上書きしないため、この pin が有効です。後続の動的 XSETTING による置き換えは、攻撃者の文字列をコピーする前に return します。
初期 XSETTINGS による bypass
ただし pin だけでは 1 つの隙間が残っていました。gtk_init_check() 自身が X サーバに接続して初期 XSETTINGS バッチを処理するためです。早期の X11 接続を持つ publisher —— まさに GPU プロセスが持っているもの —— は、ブラウザの GTK 初期化前に Gtk/Modules を配置でき、GTK は pin が適用される前の init 中にモジュールをロードしてしまいます。
、CL 8296244(issue 552652382)がこの隙間を main 上で閉じました。内容は 3 点です:
GtkSettingsSetProperty()内でgtk-modules書き込み自体をサニタイズ —— setter 経路を通る値はすべて""に強制される- 設定 interceptor を早期、つまり
gtk_init_check()の前(およびgtk_util.ccのGtkInitCheckラッパ内部)にインストールし、初期 XSETTINGS の読み取りもインターセプトする GTK_MODULES環境変数を unset し、同じモジュールローダへの環境変数経路も閉じる
現在の Chromium main は多層防御になっています。post-init の動的更新には APPLICATION-source の pin、初期化時の経路には早期 interceptor によるサニタイズが効いています。
ブランチ側の揺れ
元の pin CL の cherry-pick は から 08-25 にかけて M152(branch-heads/7977)、M151(branch-heads/7922)、7922_139、M144(branch-heads/7559)にマージされました。その後、一部のブランチでは revert されています。7922_139 の revert は に、M152 の revert は にマージされ、M151・M144・M153 向けの revert CL は執筆時点で pending のままです。revert の根拠は制限付き issue(551107198)で追跡されています。issue には引き続きマイルストーン M151、Security_Impact-Stable、リリースノート対象 4-M151 が付いており、ChromeOS LTS ラベルも同じ揺れを反映しています(LTS-Merge-Merged-144、後に LTS-NotApplicable-150)。
CVE-2026-76023 は に採番され、VRP パネルは に $5,000 の報奨金を決定しました(根拠: local privilege escalation)。 に issue のアクセス制限が解除され、公開状態となっています。
影響
最終的なプリミティブは、BPF サンドボックス化された GPU プロセスの侵害済み状態から、Chrome のサンドボックス外ブラウザプロセスにおける制御されたネイティブモジュール初期化です。クラッシュやパーサレベルの挙動よりも強力です。GTK は意図的に供給された ELF を実行可能にマップし、攻撃者の初期化関数を呼びます。ブラウザプロセスの侵害は GPU サンドボックス内に閉じたコードよりも大幅に高い権限を持ち、被害者アカウントで利用可能なブラウザプロセスの能力を露出させます。独立した renderer→GPU exploit があれば、これをチェーンの後段として利用できます。
実証された範囲は意図的に限定しています: x86-64 Linux、Ozone/X11、GTK3、保持された認証済み X11 接続、互換なモジュール ABI、テスト済みの同一 UID /proc/<pid>/fd ポリシー(hidepid、PID namespace、Yama/LSM ポリシーでこの細部は変わりえます)。Wayland、GTK4、Linux 以外のプラットフォーム、renderer→GPU の前段、Web 到達可能なトリガー、完全なリモート攻撃チェーンは主張しません。正確には「Linux Ozone/X11/GTK3 における侵害済み GPU→ブラウザ サンドボックス脱出プリミティブの source-complete な分割証明」であり、Google は S1 とトリアージし、サンドボックス脱出として報奨金を支払いました。
タイムライン
| 日付 | 出来事 |
|---|---|
| 問題を初めて特定・検証(Chrome 150.0.7871.181 製品シンク、standalone GTK および GPU ポリシーキャンペーン) | |
| 証拠パッケージを集約。未改変製品キャンペーンが Chrome for Testing 152.0.7973.0 で 10/10 | |
| Chrome for Testing Beta 152.0.7977.30 で再検証(A/B/R + 実 GPU 判別子 + 候補パッチ N/R)。Chrome VRP に報告 → issue 545124048 | |
修正が main にマージ: gtk-modules を "" に pin(CL 8243490) | |
| Severity が S1 に設定。M151/M152 へのマージリクエスト | |
| M152(7977)および M151(7922)に cherry-pick がマージ。M144 へのマージリクエスト | |
| 7922_139 へのマージ完了 | |
| CVE-2026-76023 採番。リリースノート対象 4-M151 | |
| M144(7559)へのマージ完了 | |
最初のブランチ revert がマージ(7922_139)。より完全な修正が main にマージ(CL 8296244、issue 552652382): 早期 interceptor + gtk-modules サニタイズ + GTK_MODULES unset | |
| VRP パネルが $5,000 を決定(local privilege escalation) | |
| M152(7977)の revert がマージ。M151/M144/M153 の revert は pending | |
| issue のアクセス制限が解除(公開) | |
LTS-NotApplicable-150 が付与(パッチがブランチから revert されたため) | |
| 本記事を執筆 |
報告者: Keita Sode(SYZD Research)。迅速なトリアージと修正作業を行っていただいた Chrome セキュリティチームおよび Thomas Anderson 氏に感謝します。なお、検証用の PoC モジュールはプロセス内で PID マーカーを書き込むのみで、シェルの起動、ブラウザデータの読み取り、外部サービスへの通信、ペイロードの永続化等は行いません。