Skip to content

Commit db13d7d

Browse files
linesightclaude
andcommitted
refactor(linux): audit cef.Initialize defaults; drop dead switches and stale workarounds
- Drop dead/no-op code: duplicate ozone-platform setdefault, sw alias, wayland-branch's setdefault, redundant GDK_BACKEND set (already in _linux_gtk_init), and the WAYLAND_DISPLAY pop (--ozone-platform=x11 in argv already wins). - Drop empirically-unnecessary switches: disable-zygote, disable-gpu, profile-directory, disable-features=ProfilePicker..., and six noise switches (no-first-run, disable-sync, no-startup-window, disable-background-networking, password-store, disable-dev-shm-usage). Verified across hello_world.py / pysdl2.py / qt.py / native Wayland. - Remove _linux_setup_profile() and its call site; the profile-picker keepalive workaround does not reproduce on current CEF/Chromium even with a fresh user-data-dir. - Make VK_ICD_FILENAMES SwiftShader fallback conditional on no system Vulkan ICD being installed (glob /usr/share/vulkan/icd.d/), so bare-metal users keep hardware Vulkan instead of being silently swapped to software rendering. - Replace version-tagged comments with current-tense rationale; add upstream history blocks for the no-sandbox default (CEF commit f5bc72b23 / branch 2357, Ubuntu 23.10 AppArmor regression) and for the absent CefCurrentlyOn(TID_UI) assert in CreateBrowserSync. - Knowledge-Base.md: add troubleshooting entry for the "kTransientFailure: CreateCommandBuffer" log line, and an opt-in walkthrough for enabling the Chromium sandbox by installing chrome-sandbox SUID-root manually. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bb24151 commit db13d7d

3 files changed

Lines changed: 178 additions & 139 deletions

File tree

docs/Knowledge-Base.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ Table of contents:
1313
* [How to capture Audio and Video in HTML5?](#how-to-capture-audio-and-video-in-html5)
1414
* [Touch and multi-touch support](#touch-and-multi-touch-support)
1515
* [Black or white browser screen](#black-or-white-browser-screen)
16+
* ["kTransientFailure: Failed to send GpuControl.CreateCommandBuffer" on Linux](#ktransientfailure-failed-to-send-gpucontrolcreatecommandbuffer-on-linux)
1617
* [Python crashes with "Segmentation fault" - how to debug?](#python-crashes-with-segmentation-fault---how-to-debug)
1718
* [Windows XP support](#windows-xp-support)
1819
* [Mac 32-bit support](#mac-32-bit-support)
1920
* [Security](#security)
21+
* [Linux: enabling the Chromium sandbox (advanced)](#linux-enabling-the-chromium-sandbox-advanced)
2022

2123

2224
## Notifications about new releases / commits
@@ -264,6 +266,33 @@ appear even after disabling GPU hardware acceleration. This is normal
264266
because GPU was disabled so WebGL cannot work.
265267

266268

269+
## "kTransientFailure: Failed to send GpuControl.CreateCommandBuffer" on Linux
270+
271+
You may see a log line like this during startup, especially on Linux
272+
VMs and other systems without a working GPU. On VMs the line appears in
273+
nearly every run; on bare metal with a real GPU it is rarer:
274+
275+
```
276+
ERROR:gpu/ipc/client/command_buffer_proxy_impl.cc:285] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer.
277+
```
278+
279+
**This is a Chromium-recoverable transient and can be ignored.** It is
280+
emitted by the renderer process when it tries to create a GPU command
281+
buffer before the GPU process has finished binding its IPC endpoint —
282+
typically a millisecond-scale race during startup, more likely on slow
283+
disks or when Chromium falls back from real-GL to SwiftShader. The
284+
compositor retries automatically; pages still render and
285+
`OnContextInitialized` still fires. The `kTransientFailure` label is
286+
Chromium's own classification — Chromium expects callers to retry, and
287+
they do.
288+
289+
If a clean log is more important than hardware acceleration in your
290+
deployment, you can opt in to disabling the GPU process by passing
291+
`switches={"disable-gpu": ""}` to `cef.Initialize()`. Do not enable
292+
`in-process-gpu` to silence this line — it is not stable across
293+
multiple browser windows.
294+
295+
267296
## Python crashes with "Segmentation fault" - how to debug?
268297

269298
Install gdb:
@@ -347,3 +376,59 @@ A quote by Marshall Greenblatt:
347376
Reference: [Question on browser security](http://magpcss.org/ceforum/viewtopic.php?f=10&t=10222)
348377
on the CEF Forum.
349378

379+
380+
### Linux: enabling the Chromium sandbox (advanced)
381+
382+
cefpython on Linux passes `--no-sandbox` by default
383+
(`_linux_apply_initialize_defaults` in `src/window_utils_linux.pyx`).
384+
385+
Reasons for the default:
386+
1. cefpython does not bundle the SUID-root `chrome-sandbox` helper that
387+
Chromium's namespace sandbox needs to bypass AppArmor's
388+
`apparmor_restrict_unprivileged_userns=1` (the default on Ubuntu
389+
23.10+, Debian 12+, and other modern distros).
390+
2. Without the helper, Chromium aborts at startup with
391+
`FATAL: No usable sandbox!` — every cefpython app would fail to
392+
launch out of the box.
393+
3. The most common cefpython use case is rendering the application's
394+
own trusted HTML/JS as a UI surface, where the sandbox provides
395+
defense-in-depth rather than primary security.
396+
397+
If your app loads untrusted web content and you want the Chromium
398+
sandbox enabled, three steps are required:
399+
400+
**1. Install the SUID-root `chrome-sandbox` helper.**
401+
The binary ships in the CEF binary distribution under
402+
`Release/chrome-sandbox`. Copy it to a stable path and set it up:
403+
404+
```bash
405+
sudo cp /path/to/cef_binary_<ver>_linux64/Release/chrome-sandbox \
406+
/opt/cef/chrome-sandbox
407+
sudo chown root:root /opt/cef/chrome-sandbox
408+
sudo chmod 4755 /opt/cef/chrome-sandbox
409+
```
410+
411+
**2. Tell Chromium where the helper lives.** Set the
412+
`CHROME_DEVEL_SANDBOX` environment variable before `cef.Initialize()`:
413+
414+
```python
415+
import os
416+
os.environ["CHROME_DEVEL_SANDBOX"] = "/opt/cef/chrome-sandbox"
417+
```
418+
419+
**3. Remove cefpython's `--no-sandbox` default.** Because
420+
`_linux_apply_initialize_defaults` sets it via `setdefault`, passing
421+
`switches={"no-sandbox": ""}` to `cef.Initialize()` does not override
422+
it. The default has to be deleted at the source (or pop the key from
423+
your switches dict after init detection). The simplest path is a small
424+
local patch in `src/window_utils_linux.pyx` that drops the
425+
`cmd_switches.setdefault("no-sandbox", "")` line.
426+
427+
After all three steps, Chromium subprocesses should launch under the
428+
namespace sandbox (no `--no-sandbox` in their argv, no FATAL at
429+
startup). Verify via the GPU process command line:
430+
431+
```bash
432+
ps -Af | grep "type=gpu" | grep -v "no-sandbox"
433+
```
434+

src/cefpython.pyx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -587,10 +587,6 @@ def Initialize(applicationSettings=None, commandLineSwitches=None, **kwargs):
587587
# In native Wayland mode, no GTK/X11 connection is needed or wanted.
588588
if not _g_linux_wayland_mode:
589589
_linux_gtk_init()
590-
# Pre-seed Chrome profile files to prevent the profile-picker keepalive
591-
# from blocking OnContextInitialized (Chrome 146).
592-
if application_settings.get("cache_path"):
593-
_linux_setup_profile(application_settings["cache_path"])
594590

595591
cdef CefRefPtr[CefApp] cefApp = <CefRefPtr[CefApp]?>new CefPythonApp()
596592

@@ -726,8 +722,14 @@ def CreateBrowserSync(windowInfo=None,
726722
raise Exception("Invalid argument: "+kwarg)
727723

728724
Debug("CreateBrowserSync() called")
729-
# CEF 146+: CefCurrentlyOn(TID_UI) returns false before MessageLoop starts,
730-
# so skip the assert here and let CEF's own internal checks handle it.
725+
# No CefCurrentlyOn(TID_UI) assert here. cefpython defers the real
726+
# browser creation until OnContextInitialized fires (see below), so
727+
# this function is reached before BrowserThread::UI is fully
728+
# established — at which point CefCurrentlyOn() returns false and
729+
# logs a WARNING (libcef/common/task_impl.cc). CEF's own
730+
# CefBrowserHost::CreateBrowserSync() runs CONTEXT_STATE_VALID()
731+
# plus its own thread checks internally, so a Python-side assert
732+
# would only catch the same condition with a worse error message.
731733

732734
# Defer browser creation until OnContextInitialized fires inside MessageLoop.
733735
# In CEF 123+, browser creation before OnContextInitialized causes

src/window_utils_linux.pyx

Lines changed: 85 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def _linux_get_root_xid():
9999

100100

101101
def _linux_apply_initialize_defaults(app_settings, cmd_switches):
102-
"""Auto-apply Linux CEF 146 defaults that every app needs.
102+
"""Auto-apply Linux defaults that every cefpython app needs.
103103
104104
X11/XWayland is the default on all Linux systems (even Wayland sessions).
105105
Native Wayland mode must be requested explicitly by passing
@@ -112,147 +112,99 @@ def _linux_apply_initialize_defaults(app_settings, cmd_switches):
112112
manager decorates the GTK frame window normally.
113113
114114
Uses setdefault so users can still override any individual entry by passing
115-
it explicitly to cef.Initialize(switches={...}).
115+
it explicitly to cef.Initialize(switches={...}). Each setting kept here
116+
has been individually retested against current CEF/Chromium — anything
117+
that did not regress when removed has been dropped.
116118
"""
117119
global _g_linux_wayland_mode
118120
import os as _os
119121

120-
# Native Wayland mode only when the caller explicitly opts in.
121-
_ozone_explicit = cmd_switches.get("ozone-platform", "")
122-
_wayland_mode = (_ozone_explicit == "wayland")
123-
_g_linux_wayland_mode = _wayland_mode
124-
125-
if not _wayland_mode:
126-
# X11/Xwayland mode: force GDK and Chrome onto X11.
127-
# Must be set before gtk_init() so GDK opens an X11/Xwayland display.
128-
_os.environ.setdefault("GDK_BACKEND", "x11")
129-
# Remove WAYLAND_DISPLAY so Chrome's Ozone platform selection picks X11.
130-
_os.environ.pop("WAYLAND_DISPLAY", None)
122+
# Native Wayland mode only when the caller explicitly opts in via
123+
# switches={"ozone-platform": "wayland"}. Otherwise default to X11/Xwayland.
124+
_g_linux_wayland_mode = (cmd_switches.get("ozone-platform") == "wayland")
125+
126+
if not _g_linux_wayland_mode:
127+
# X11/Xwayland mode: force Chrome's Ozone backend to X11. This is
128+
# the *only* thing keeping Chromium off the Wayland display on a
129+
# Wayland session — we cannot embed a Wayland xdg_surface inside
130+
# the GTK X11 window we create in _linux_create_toplevel().
131+
# GDK_BACKEND=x11 is set separately in _linux_gtk_init() before
132+
# gtk_init().
131133
cmd_switches.setdefault("ozone-platform", "x11")
132-
else:
133-
# Native Wayland mode: use the Ozone Wayland backend.
134-
# Keep WAYLAND_DISPLAY so Chrome connects to the Wayland compositor.
135-
cmd_switches.setdefault("ozone-platform", "wayland")
136-
137-
# Point the Vulkan loader at the SwiftShader ICD shipped with CEF.
138-
import cefpython3 as _cef3_pkg
139-
_cef3_dir = _os.path.dirname(_cef3_pkg.__file__)
140-
_vk_icd = _os.path.join(_cef3_dir, "vk_swiftshader_icd.json")
141-
if _os.path.exists(_vk_icd):
142-
_os.environ.setdefault("VK_ICD_FILENAMES", _vk_icd)
143-
144-
# CEF 146 on Ozone X11 requires a running GLib main loop for
145-
# OnContextInitialized to fire. external_message_pump integrates
146-
# CefDoMessageLoopWork() as a GLib source.
134+
# Native Wayland branch: "ozone-platform" is already in cmd_switches
135+
# (the only way to reach this branch), so nothing to do here.
136+
137+
# Vulkan ICD fallback for systems with no system-installed driver.
138+
#
139+
# On systems with no Vulkan ICD (typical for VMs and minimal containers),
140+
# Chromium's GPU process fails its Vulkan probe and the renderer logs a
141+
# transient
142+
# ContextResult::kTransientFailure: Failed to send
143+
# GpuControl.CreateCommandBuffer
144+
# before falling back to software rendering. Pointing VK_ICD_FILENAMES
145+
# at the SwiftShader manifest bundled with CEF makes the probe succeed
146+
# immediately and silences the line.
147+
#
148+
# Only apply the fallback when no system ICD is present in the standard
149+
# loader search paths — overriding a working Mesa/NVIDIA/AMD ICD with
150+
# SwiftShader would force software rendering for no reason on real GPUs.
151+
# Honors a pre-set VK_ICD_FILENAMES (setdefault) so users can override.
152+
import glob as _glob
153+
_system_icds = (_glob.glob("/usr/share/vulkan/icd.d/*.json") +
154+
_glob.glob("/etc/vulkan/icd.d/*.json") +
155+
_glob.glob("/usr/local/share/vulkan/icd.d/*.json"))
156+
if not _system_icds:
157+
import cefpython3 as _cef3_pkg
158+
_cef3_dir = _os.path.dirname(_cef3_pkg.__file__)
159+
_vk_icd = _os.path.join(_cef3_dir, "vk_swiftshader_icd.json")
160+
if _os.path.exists(_vk_icd):
161+
_os.environ.setdefault("VK_ICD_FILENAMES", _vk_icd)
162+
163+
# _linux_message_loop / _linux_wayland_message_loop drive CEF by calling
164+
# CefDoMessageLoopWork() from a GLib timer. external_message_pump tells
165+
# CEF not to run its own internal loop, so the two don't conflict.
147166
app_settings.setdefault("external_message_pump", True)
148167

149-
# Allow per-browser opt-in to off-screen rendering. This is needed so
150-
# that JS-created popup browsers (window.open) can be closed without
151-
# dispatching GLib/X11 events: off-screen browsers are destroyed
152-
# immediately when DoClose returns False (no delete_event to parent).
168+
# Allow per-browser opt-in to off-screen rendering. Required by examples
169+
# that pass WindowInfo.SetAsOffscreen() (e.g. pysdl2.py) and by JS-created
170+
# popup browsers, which are destroyed immediately when DoClose returns
171+
# False — no delete_event would be dispatched on a windowed popup.
153172
app_settings.setdefault("windowless_rendering_enabled", True)
154173

155-
# Chromium switches required for stable embedded operation on CEF 146.
156-
sw = cmd_switches
157-
# Ozone platform: already set above (wayland or x11); setdefault is a no-op
158-
# if the user already passed ozone-platform explicitly.
159-
sw.setdefault("ozone-platform", "x11")
160-
# Bypass Zygote to avoid stack-smash crash from --change-stack-guard-on-fork.
161-
sw.setdefault("disable-zygote", "")
162-
# Belt-and-suspenders sandbox suppression.
163-
sw.setdefault("no-sandbox", "")
164-
# /dev/shm may be too small in VMs and containers.
165-
sw.setdefault("disable-dev-shm-usage", "")
166-
# Suppress GNOME Keyring unlock prompt.
167-
sw.setdefault("password-store", "basic")
168-
# Skip 3× GPU subprocess crash cycle; go straight to software rendering.
169-
sw.setdefault("disable-gpu", "")
170-
# Startup / sync / background noise suppression.
171-
sw.setdefault("no-first-run", "")
172-
sw.setdefault("disable-sync", "")
173-
sw.setdefault("no-startup-window", "")
174-
sw.setdefault("disable-background-networking", "")
175-
# Profile subdirectory — prevents ShouldShowProfilePickerAtLaunch() from
176-
# returning True and adding a kProfileCreationFlow keepalive that would
177-
# permanently block OnContextInitialized in embedded apps.
178-
sw.setdefault("profile-directory", "Default")
179-
# Disable UI features that add their own keepalives or block init.
180-
if "disable-features" not in sw:
181-
sw["disable-features"] = (
182-
"WebGPU,"
183-
"ProfilePicker,"
184-
"ProfilePickerIPH,"
185-
"ForYouFre,"
186-
"SyncPromoFRE,"
187-
"ChromeSigninIphExperiment,"
188-
"ChromeWhatsNewUI,"
189-
"DefaultBrowserPrompt,"
190-
"ProfileManagementFlowController"
191-
)
192-
193-
194-
def _linux_setup_profile(cache_path):
195-
"""Pre-create Chrome profile files to skip profile-picker keepalive.
196-
197-
Chrome 146 adds a kProfileCreationFlow keepalive when it creates a new
198-
profile from scratch and only removes it after the wizard UI completes.
199-
Writing seed files before cef.Initialize() makes Chrome treat the
200-
profile as already configured, skipping the keepalive entirely.
201-
"""
202-
import os as _os, json as _json, glob as _glob
203-
204-
default_dir = _os.path.join(cache_path, "Default")
205-
_os.makedirs(default_dir, exist_ok=True)
206-
207-
for _pat in ("Singleton*", "*.lock", "LOCK"):
208-
for _f in _glob.glob(_os.path.join(cache_path, _pat)):
209-
try: _os.unlink(_f)
210-
except OSError: pass
211-
for _f in _glob.glob(_os.path.join(default_dir, _pat)):
212-
try: _os.unlink(_f)
213-
except OSError: pass
214-
215-
first_run = _os.path.join(cache_path, "First Run")
216-
if not _os.path.exists(first_run):
217-
open(first_run, "w").close()
218-
219-
local_state = _os.path.join(cache_path, "Local State")
220-
if not _os.path.exists(local_state):
221-
with open(local_state, "w") as _f:
222-
_json.dump({"profile": {
223-
"info_cache": {"Default": {
224-
"active_time": 1704067200.0,
225-
"avatar_icon": "chrome://theme/IDR_PROFILE_AVATAR_0",
226-
"is_using_default_avatar": True,
227-
"is_using_default_name": True,
228-
"is_new_profile": False,
229-
"managed_user_id": "",
230-
"name": "Default",
231-
}},
232-
"last_used": "Default",
233-
"profiles_created": 1,
234-
}}, _f)
235-
236-
prefs = _os.path.join(default_dir, "Preferences")
237-
if not _os.path.exists(prefs):
238-
with open(prefs, "w") as _f:
239-
_json.dump({
240-
"profile": {
241-
"creation_time": "13328563200000000",
242-
"is_using_default_name": True,
243-
"name": "Default",
244-
},
245-
"browser": {"has_seen_welcome_page": True},
246-
"privacy_sandbox": {
247-
"m1.consent_decision_made": True,
248-
"m1.notice_acknowledged": True,
249-
"m1.restricted_notice_acknowledged": True,
250-
"consent_decision_made": True,
251-
"notice_acknowledged": True,
252-
"first_run_consent_required": False,
253-
"first_run_setup_complete": True,
254-
},
255-
}, _f)
174+
# Disable Chromium's Linux sandbox.
175+
#
176+
# History (so readers don't think this is CEF-146-specific):
177+
# * CEF defaulted sandbox-OFF until 2013-11-15 (commit f5bc72b23,
178+
# SVN trunk@1518, "Add sandbox support, issue #524"). The
179+
# commit message stated explicitly: "Linux: For binary
180+
# distributions a new chrome-sandbox executable with SUID
181+
# permissions must be placed next to the CEF executable."
182+
# * The earliest release branch carrying that change is branch
183+
# 2357 (created 2015-08-21, ~Chromium 44). From 2357 onward,
184+
# every CEF Linux build defaults sandbox-ON and refuses to start
185+
# unless either a SUID-root chrome-sandbox helper is installed
186+
# or this --no-sandbox switch is passed.
187+
# * For ~2017-2023, most distros enabled
188+
# kernel.unprivileged_userns_clone=1, so Chromium's namespace
189+
# sandbox could substitute for the SUID helper and many
190+
# embedders quietly avoided either step.
191+
# * Ubuntu 23.10 (Oct 2023) set
192+
# kernel.apparmor_restrict_unprivileged_userns=1 by default;
193+
# Debian 12 followed. This re-broke the namespace-only path:
194+
# without the SUID helper Chromium aborts with
195+
# FATAL: No usable sandbox! (zygote_host_impl_linux.cc:128)
196+
# * cefpython is distributed as a pip wheel, and pip cannot
197+
# chown root + chmod 4755 a binary (it runs as the user, not
198+
# root, and has no postinst hook). Distro packages such as
199+
# google-chrome and chromium handle this in their .deb/.rpm
200+
# postinst scripts; cefpython has no equivalent install path.
201+
#
202+
# Effect of this line: cefpython works out of the box on every
203+
# Linux distro at the cost of running Chromium subprocesses
204+
# without the chromium namespace sandbox (seccomp-bpf still
205+
# applies). See docs/Knowledge-Base.md "Linux: enabling the
206+
# Chromium sandbox" for the manual opt-in path.
207+
cmd_switches.setdefault("no-sandbox", "")
256208

257209

258210
def _linux_create_toplevel(title, width=_LINUX_DEFAULT_WIDTH, height=_LINUX_DEFAULT_HEIGHT):

0 commit comments

Comments
 (0)