The Unguarded Setting: GPU-to-Browser Sandbox Escape in Chrome via GTK3's Gtk/Modules XSETTING

CVE
CVE-2026-76023
Severity
S1 (High), Security_Impact-Stable
Chromium Issue
545124048
VRP Reward
$5,000

: Reported to Chrome VRP (issue 545124048)
: Fix landed on Chromium main (CL 8243490)
: CVE-2026-76023 registered
: Chrome VRP panel awarded $5,000
: Issue access restrictions removed (public)

This article discusses a Linux/X11 sandbox-escape vulnerability I reported to Chrome VRP: a compromised GPU process could make GTK3 — running inside Chrome's unsandboxed browser process — load and initialize an attacker-controlled ELF staged in an executable memfd, through a single XSETTING named Gtk/Modules.

In short, Chrome on Ozone/X11 intentionally creates the GPU process's X11 connection before the GPU seccomp-BPF sandbox is installed, because opening the connection later would require blocked socket()/connect() calls. A compromised GPU process can use that retained, authenticated connection to become the XSETTINGS manager and publish Gtk/Modules=/proc/<gpu-pid>/fd/<fd>. GTK3 in the browser process maps that wire name to the gtk-modules property, copies the value around Chrome's set_property interceptor, accepts the absolute path, calls g_module_open(), resolves gtk_module_init, and invokes it — native code execution in the browser process.

A few aspects of this issue are worth highlighting:

Background

The GPU/browser boundary on X11

Chrome's Linux process model treats the GPU process as less trusted than the browser process: it runs under a seccomp-BPF policy, and the security requirement is that a compromise there must not become native code execution in the unsandboxed browser process.

On Ozone/X11, however, graphics initialization requires a live X11 connection. Chromium therefore creates that connection before installing the GPU sandbox. In ui/ozone/platform/x11/ozone_platform_x11.cc, OzonePlatformX11::InitializeGPU() makes the ordering explicit:

// 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));

This connection is necessary, but it is also an authority channel. Under the XSETTINGS protocol, any X11 client can claim the per-screen _XSETTINGS_S<n> selection, attach an _XSETTINGS_SETTINGS property to its owner window, and announce that owner with a MANAGER client message. GTK applications on the same display then consume whatever the selected manager publishes.

Gtk/Modules is executable configuration

GTK3's X11 settings map translates the wire-level Gtk/Modules name into the GObject property gtk-modules (gdk/x11/gdksettings.c):

{"Gtk/KeyThemeName",        "gtk-key-theme-name"},
{"Gtk/Modules",             "gtk-modules"},
{"Gtk/ButtonImages",        "gtk-button-images"},

This is not a cosmetic setting. A nonempty gtk-modules value names one or more native GTK modules. GTK loads each module and calls its exported gtk_module_init entry point in the process hosting GTK. In Chrome's X11 configuration, that host is the unsandboxed browser process.

Existing defenses covered only theme-like properties

Chromium already recognizes that a compromised GPU process can abuse its pre-sandbox X11 connection. GtkUi::Initialize() obtains the default GtkSettings, sanitizes theme-related values, and installs a GtkSettings::set_property interceptor. The interceptor in ui/gtk/gtk_util.cc handled only:

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;
}

There was no corresponding policy for gtk-modules.

Investigation

The starting point was a series of public Chromium fixes that explicitly established the compromised-GPU/XSETTINGS threat model:

Those changes sanitize parsed data — names that feed theme engines and image decoders. Auditing the XSETTINGS-to-property map against them showed that the one property which directly selects executable code, Gtk/Modules, was not covered by any of them.

The vulnerable path was confirmed end-to-end against the exact runtime: an unmodified Chrome for Testing Beta 152.0.7977.30 binary and the AlmaLinux gtk3-3.24.43-5.el10.x86_64 package, whose source RPM was verified byte-identical to the inspected upstream GTK 3.24.43 tarball for the three files on the load path (gdksettings.c, gtksettings.c, gtkmodules.c). Earlier versions of the same campaign had already reproduced the sink on official Chrome 150.0.7871.181.

Vulnerability Details

1. A compromised GPU retains the control channel

The threat model begins with native code execution inside Chrome's BPF-sandboxed GPU process. The retained pre-sandbox X11 connection lets that process claim _XSETTINGS_S0 and publish a well-formed XSETTINGS payload — no new socket and no malformed X11 packet is needed.

ObjectProcessAttacker control
XSETTINGS owner windowGPUFull
_XSETTINGS_SETTINGS bytesGPUFull
Setting nameGPUGtk/Modules
Setting valueGPUArbitrary string
GtkSettings consumerBrowserPrivileged sink

2. The GPU policy permits a fileless executable payload

The payload does not need a persistent file on disk. Chromium's GPU BPF policy allows prctl and passes memfd_create through the executable-mapping variant of the memfd restriction (sandbox/policy/linux/bpf_gpu_policy_linux.cc):

case __NR_prctl:
  return Allow();
// ...
case __NR_memfd_create:
  return RestrictMemfdCreateWithExecMappings();

RestrictMemfdCreateWithExecMappings() explicitly admits MFD_EXEC (sandbox/linux/seccomp-bpf-helpers/syscall_parameters_restrictions.cc). The attacker sequence is therefore:

  1. memfd_create("name", MFD_ALLOW_SEALING | MFD_EXEC) and write a position-independent GTK module into the descriptor;
  2. prctl(PR_SET_DUMPABLE, 1) so the same-UID browser can traverse /proc/<gpu-pid>/fd/;
  3. publish Gtk/Modules=/proc/<gpu-pid>/fd/<fd> and keep the owner window and descriptor alive while the browser UI thread handles the MANAGER event.

3. GTK copies the dynamic value without passing through Chrome's interceptor

When GTK receives the XSETTINGS change, _gtk_settings_handle_event() looks up the property and calls settings_update_xsetting(). For string-valued properties, that function obtains the wire-provided value and copies it directly into 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;

This is the crucial transition: it does not call the GObject set_property vtable that Chromium interposes. Merely adding gtk-modules to GtkSettingsSetProperty() would therefore have left the dynamic XSETTING route open.

The subsequent class notification is already a privileged consumer: gtk_settings_notify() handles PROP_MODULES by calling settings_update_modules(), which forwards the attacker string to _gtk_modules_settings_changed() before any application-level notify::gtk-modules callback could sanitize it.

4. GTK accepts the proc-fd path and calls attacker code

GTK's module resolver deliberately accepts absolute paths:

if (g_path_is_absolute (name))
  return g_strdup (name);

The loader then opens it and invokes the module initializer:

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) (&gtk_argc, &gtk_argv);

At this point the browser process has mapped attacker-controlled executable bytes and transferred native control to them. There is no memory-corruption crash to symbolize: the security failure is an intended module-loading path reached with input from a less-privileged compromised process.

The violated invariant is:

Data controlled by a compromised GPU process must not select executable code that the unsandboxed browser process loads and invokes.

Verification

Because this is a logic-based native module-loading path rather than memory corruption, validation focused on process identity and executable-mapping discriminators rather than crashes.

Unmodified product sink: A/B/R campaign

Against an unmodified Chrome for Testing Beta 152.0.7977.30 binary (SHA-256 d044c928…6e79aa), with fresh Xvfb displays and profiles, normal sandbox settings, and no LD_PRELOAD, --no-sandbox, or --disable-gpu-sandbox:

AxisBrowser PIDPublisher PIDMarker PIDExecutable target mappingResult
A — publish Gtk/Modules memfd630499630684630499memfd inode 126305, r-xppass
B — no publication630735nonenonenonepass
R — fresh repeat631011631229631011memfd inode 127235, r-xppass

In both positive axes the benign module's marker PID equaled the browser PID and differed from the publisher PID, and the exact staged memfd inode was mapped r-xp in the browser — ruling out attribution to the external publisher or a stale-file explanation. The external publisher is an X11-protocol analogue of what the compromised GPU does; it proves the browser/GTK sink end-to-end on the unmodified product.

Real-GPU post-filter discriminator

A separate three-run campaign placed an LD_PRELOAD probe in Chrome's real GPU PID, bracketing Chrome's own seccomp(SECCOMP_SET_MODE_FILTER, ...) call. Under --gpu-sandbox-start-early, seccomp is installed before Ozone/X11 initialization, so the probe opened a dedicated X11 connection and source-module FD before the filter. Every run then recorded a real filtered GPU PID (NoNewPrivs: 1, Seccomp: 2, one filter) retaining an X11 socket, creating an MFD_EXEC memfd after filter installation, publishing Gtk/Modules, and causing a browser-PID gtk_module_init plus an executable browser mapping.

This campaign deliberately uses disclosed instrumentation; it establishes post-filter syscall and retained-descriptor capability under Chrome's actual GPU policy. It is not claimed as a pristine end-to-end exploit or a Web/renderer trigger.

One honest negative note: a good-faith attempt to reuse the product-created XCB connection on the Xvfb/software-rendering test host never reached its target state (dri3 extension not supported) and produced no marker. That is an environment gap, not evidence for or against the primitive — the sandbox was not weakened to force a result.

Candidate mitigation differential

Before submission I also validated the proposed fix on the exact product: two binaries were built from the same Chromium tag (cbd60d12ee30) with identical GN arguments, differing only in a candidate patch that pins gtk-modules to an empty application-owned value.

AxisXSETTINGS publicationBrowser markerTarget module mapResult
N — patchedyesnonopass
R — clean restorationyesyes, exact browser PIDyes, r-xppass

The attacker publication still succeeded against the patched build — the setting simply no longer became code loading. Removing the patch restored both outcomes.

Fix and Rollout

The issue was reported on and assigned to a Chromium owner the same night. On , CL 8243490 landed on main — exactly the proposed minimal fix:

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);

This works because GTK3 records a g_object_set() value with GTK_SETTINGS_SOURCE_APPLICATION precedence, and settings_update_xsetting() refuses to overwrite application-sourced values — so later dynamic XSETTING replacement returns before copying the attacker string.

The initial-XSETTINGS bypass

The pin alone left one window: gtk_init_check() itself connects to the X server and processes the initial XSETTINGS batch. A publisher holding an early X11 connection — exactly what the GPU process has — can have Gtk/Modules in place before the browser's GTK initialization, so GTK loads the module during init, before the pin is ever applied.

On , CL 8296244 (issue 552652382) closed that window on main. It does three things:

  1. Sanitizes gtk-modules writes inside GtkSettingsSetProperty() itself — any value set through the setter path is forced to "";
  2. Installs the settings interceptor early, before gtk_init_check() runs (and inside the GtkInitCheck wrapper in gtk_util.cc), so the initial XSETTINGS read is intercepted;
  3. Unsets the GTK_MODULES environment variable, closing the environment-variable path to the same module loader.

Current Chromium main therefore carries layered defense: the APPLICATION-source pin for post-init dynamic updates, plus early interceptor sanitization for the initialization-time path.

Branch churn

The cherry-picks of the original pin CL were merged to M152 (branch-heads/7977), M151 (branch-heads/7922), 7922_139, and M144 (branch-heads/7559) between and . They were then reverted on some branches — the 7922_139 revert merged on and the M152 revert merged on — while the M151, M144, and M153 revert CLs were still pending at the time of writing, with the revert rationale tracked in a restricted issue (551107198). The issue retains milestone M151, Security_Impact-Stable, and release-notes target 4-M151; the ChromeOS LTS labels reflect the same churn (LTS-Merge-Merged-144, later LTS-NotApplicable-150).

CVE-2026-76023 was registered on , the VRP panel awarded $5,000 on (rationale: local privilege escalation), and the issue's access restrictions were removed on , making it public.

Impact

The final primitive is controlled native module initialization in Chrome's unsandboxed browser process, starting from an already-compromised BPF-sandboxed GPU process. It is stronger than a crash or parser-level behavior: GTK intentionally maps the supplied ELF executable and calls the attacker's initializer. Browser-process compromise is materially more privileged than code confined to the GPU sandbox and can expose browser-process capabilities under the victim's account. An independent renderer-to-GPU exploit could use this as a later chain stage.

The demonstrated scope is intentionally bounded: x86-64 Linux, Ozone/X11, GTK3, a retained authenticated X11 connection, a compatible module ABI, and the tested same-UID /proc/<pid>/fd policy (hidepid, PID namespaces, or Yama/LSM policy may alter that detail). I do not claim Wayland, GTK4, non-Linux platforms, a renderer-to-GPU first stage, a Web-reachable trigger, or a complete remote exploitation chain. Accurately, this is a source-complete split proof of a compromised GPU-to-browser sandbox-escape primitive — Google triaged it S1 and rewarded it as a sandbox escape.

Timeline

DateEvent
Issue first isolated and validated (Chrome 150.0.7871.181 product sink; standalone GTK and GPU-policy campaigns)
Evidence package consolidated; 10/10 unmodified-product campaign on Chrome for Testing 152.0.7973.0
Re-verified on Chrome for Testing Beta 152.0.7977.30 (A/B/R + real-GPU discriminator + candidate-patch N/R); reported to Chrome VRP → issue 545124048
Fix landed on main: pin gtk-modules to "" (CL 8243490)
Severity set to S1; merge requests to M151/M152
Cherry-picks merged to M152 (7977) and M151 (7922); M144 merge requested
7922_139 merge landed
CVE-2026-76023 registered; release-notes target 4-M151
M144 (7559) merge landed
First branch revert merged (7922_139); more complete fix lands on main (CL 8296244, issue 552652382): early interceptor + gtk-modules sanitize + GTK_MODULES unset
VRP panel awards $5,000 (local privilege escalation)
M152 (7977) revert merged; M151/M144/M153 reverts pending
Issue access restrictions removed (public)
LTS-NotApplicable-150 applied (patch reverted from branches)
This article

Reporter: Keita Sode (SYZD Research). Thanks to the Chrome security team and Thomas Anderson for the quick triage and fix work. The proof-of-concept module performs only a harmless PID-marker write inside the process — it does not spawn a shell, read browser data, contact external services, or persist any payload.