Repository navigation
Description
Describe the bug
On Linux with software compositing (gpu_compositing: disabled_software), closing several browsers at the same time
often makes the GPU process log a burst of:
ERROR:gpu/command_buffer/service/shared_image/shared_image_manager.cc:385] SharedImageManager::ProduceMemory: Trying to Produce a Memory representation from a non-existent mailbox.
A burst is anywhere from 6 to about 900 lines. The close itself works: nothing crashes, every launch exits with code 0,
and the same pages close silently in Chrome style or in a single window. So the visible effect is probably limited to
a few skipped tiles in windows that are already going away. We noticed it because our app's close tests fail on any
:ERROR: line. In our app it also shows up on a real desktop after the GPU process has crashed and
Chromium has fallen back to software compositing, so it isn't only an Xvfb thing.
To Reproduce
This uses the prebuilt cefsimple from the 147.0.10 linux64 client distribution.
- Save
page3.html(below) and serve it from 127.0.0.1, e.g.python3 -m http.server 18797 --bind 127.0.0.1. - On an X11 display with software compositing (we used Xvfb with Mesa llvmpipe), start:
cefsimple --ozone-platform=x11 --use-alloy-style --use-native --no-sandbox \ --url=http://127.0.0.1:18797/page3.html#multi--no-sandboxis only there becausechrome-sandboxisn't setuid in our unpacked copy. - Click in the window three times. Each click opens one more window of the page with
noopener, so you end up with
four browsers, each with its own renderer. - 1.5 to 3.5 s later, send
WM_DELETE_WINDOWto all four windows back to back, newest first. We used the small Xlib
script below. Closing them quickly by hand might work too, but we haven't tried that. - Look for
ProduceMemoryon stderr.
page3.html, as run:
<!doctype html><meta charset="utf-8"><title>mailbox repro 2</title>
<style>
html,body{margin:0;background:#000;overflow:hidden}
#g{display:grid;grid-template-columns:repeat(20,1fr);width:100vw;height:100vh}
#g div{animation:c .23s steps(6) infinite alternate,m .4s linear infinite alternate;will-change:transform}
@keyframes c{from{background:#123}to{background:#f80}}
@keyframes m{from{transform:translateX(0)}to{transform:translateX(6px)}}
</style>
<div id="g"></div>
<script>
const g=document.getElementById("g");
for(let i=0;i<20*15;i++){const d=document.createElement("div");d.style.animationDelay=(-(i%17)*0.013)+"s,"+(-(i%7)*0.05)+"s";g.appendChild(d)}
// A page that does work while it is hidden, as a page closing a connection does: its pagehide handler holds the main
// thread for a while; the compositor keeps animating the transform layers meanwhile.
if(location.hash.includes("pagehide")){addEventListener("pagehide",()=>{const until=performance.now()+(location.hash.includes("busy")?200:0);while(performance.now()<until){}})}
// #multi: each click opens one more window of this page, as separate browsers of the same app.
let opened=0;const kids=["#child","#child-pagehide","#child"];
if(location.hash.startsWith("#multi")&&!window.opener){addEventListener("click",()=>{if(opened<kids.length){window.open("page3.html"+kids[opened],"_blank","noopener,left="+(820*(opened%2)+820)+",top="+(opened>1?620:0)+",width=800,height=600");opened++}})}
</script>wmdelete.py, which sends WM_DELETE_WINDOW to one window the way a window manager does:
import ctypes, sys
from ctypes import c_int, c_long, c_ulong, c_void_p, c_char_p
x = ctypes.cdll.LoadLibrary("libX11.so.6")
x.XOpenDisplay.restype = c_void_p; x.XOpenDisplay.argtypes = [c_char_p]
x.XInternAtom.restype = c_ulong; x.XInternAtom.argtypes = [c_void_p, c_char_p, c_int]
x.XSendEvent.argtypes = [c_void_p, c_ulong, c_int, c_long, c_void_p]
x.XFlush.argtypes = [c_void_p]; x.XCloseDisplay.argtypes = [c_void_p]
class Msg(ctypes.Structure):
_fields_ = [("type", c_int), ("serial", c_ulong), ("send_event", c_int), ("display", c_void_p),
("window", c_ulong), ("message_type", c_ulong), ("format", c_int), ("data", c_long * 5)]
class Event(ctypes.Union):
_fields_ = [("xclient", Msg), ("pad", c_long * 24)]
d = x.XOpenDisplay(None)
if not d: sys.exit("no display")
w = int(sys.argv[1], 0)
e = Event(); e.xclient.type = 33; e.xclient.window = w; e.xclient.format = 32
e.xclient.message_type = x.XInternAtom(d, b"WM_PROTOCOLS", 0)
e.xclient.data[0] = x.XInternAtom(d, b"WM_DELETE_WINDOW", 0); e.xclient.data[1] = 0
if not x.XSendEvent(d, w, 0, 0, ctypes.byref(e)): sys.exit("XSendEvent failed")
x.XFlush(d); x.XCloseDisplay(d)The loop we ran (25 launches per row below)
#!/bin/bash
# Stock cefsimple (CEF 147.0.10 client distribution) on a private Xvfb with an empty environment, loading a page from
# 127.0.0.1; three clicks open three more windows of the page; every window gets WM_DELETE_WINDOW, newest first.
set -u
R=/tmp/cef-client-147-repro/run; C=$(ls -d /tmp/cef-client-147-repro/client/*/)Release; H=/tmp/cef-client-147-repro/home
N=${1:-20}; OUT=${2:-cefsimple-multi-heavy}; PORT=18797; PAGE=${PAGE:-page3.html}; HASH=${HASH:-#multi-pagehide-busy}
mkdir -p $R/$OUT $H; cd $R
Xvfb :97 -screen 0 1920x1200x24 -nolisten tcp >xvfb.log 2>&1 & XP=$!
python3 -I -m http.server $PORT --bind 127.0.0.1 --directory $R >http.log 2>&1 & SP=$!
sleep 1
x() { env -i PATH=/usr/bin:/bin DISPLAY=:97 "$@"; }
for i in $(seq 1 $N); do
log=$OUT/run-$i.log
env -i PATH=/usr/bin:/bin HOME=$H DISPLAY=:97 XDG_SESSION_TYPE=x11 $C/cefsimple --ozone-platform=x11 --use-alloy-style \
--use-native --no-sandbox --url="http://127.0.0.1:$PORT/$PAGE$HASH" >$log 2>&1 &
P=$!
sleep 1.5; for k in 1 2 3; do x xdotool mousemove 300 300 click 1; sleep 0.4; done
W=""; for t in $(seq 1 80); do
W=$(x xwininfo -root -children 2>/dev/null | grep -E ' [0-9]{3,}x[0-9]{3,}\+' | awk '/^ +0x/{print $1}' | while read w; do x xprop -id $w WM_PROTOCOLS 2>/dev/null | grep -q WM_DELETE_WINDOW && echo $w; done | tr '\n' ' ')
[ $(echo $W | wc -w) -ge 4 ] && break; sleep 0.1; done
delay=$(python3 -c "import random;print(round(random.uniform(${DMIN:-1.5},${DMAX:-3.5}),2))"); sleep $delay
for w in $(echo $W | tr " " "\n" | sort -r); do x python3 -I wmdelete.py $w; done
ended=no; for t in $(seq 1 100); do kill -0 $P 2>/dev/null || { ended=yes; break; }; sleep 0.1; done
if [ $ended = no ]; then code=killed; kill -TERM $P; sleep 1; else wait $P; code=$?; fi
left=$(pgrep -x cefsimple | wc -l)
echo "$OUT $i windows=$(echo $W | wc -w) delay=$delay exit=$code left=$left mailbox=$(grep -c ProduceMemory $log)"
if [ $left -gt 0 ]; then for q in $(pgrep -x cefsimple); do kill -TERM $q 2>/dev/null; done; sleep 1; fi
done
kill $SP $XPResults, 25 launches each. Every launch exited with code 0.
| Setup | Launches that logged the error | Lines |
|---|---|---|
cefsimple (Alloy, native windows), four windows, #multi |
14 / 25 | 3,664 |
Same, but the first window's pagehide handler holds the main thread for 200 ms (#multi-pagehide-busy) |
15 / 25 | 4,177 |
| cefsimple (Alloy, native window), one window of the same page with the 200 ms handler | 0 / 25 | 0 |
cefsimple (Chrome style, Views windows), four windows, #multi |
0 / 25 | 0 |
Google Chrome 157.0.8103.38, four windows (--new-window), same pages |
0 / 25 | 0 |
So it seems to need Alloy style with native windows and several browsers closing at about the same time. One window
alone never logged it, and neither did Chrome style with the same pages. Every line comes from the GPU process.
Expected behavior
Closing browsers doesn't make the GPU process log errors. More concretely, we'd expect the display compositor to stop
drawing a closing browser's frame before the shared images in that frame are destroyed.
Versions:
- OS: Ubuntu 25.10, kernel 6.17.0-41. X11 through Xvfb 21.1.18 with Mesa 25.2.8 llvmpipe, which gives
gpu_compositing: disabled_software(read withSystemInfo.getInfo). - CEF Version: 147.0.10+gd58e84d+chromium-147.0.7727.118, linux64 client distribution (prebuilt cefsimple).
Additional context
Does the problem reproduce with the cefclient or cefsimple sample application at the same version?
Yes, with cefsimple, as above. We haven't tried cefclient.
Does the problem reproduce with Google Chrome at the same version?
We couldn't test Chrome 147. Chrome 157.0.8103.38 on the same display, with the same pages, didn't log it in 25
launches. cefsimple 147 in Chrome style didn't either, so our guess is that it's specific to the Alloy native-window
close path rather than something Chromium fixed after 147. That's an inference from these runs, not something we
traced.
How we found it: our app (built on Electrobun) embeds four Alloy browsers as X11 children with
CefWindowInfo::SetAsChild and calls CloseBrowser(true) on each when it quits. Its close test fails in about 2 of
17 launches. A 286-line CEF program that does only that, with a similar page, logged it in 9 of 25 launches. We can
share it if it would help.
Where we think it comes from. This is from reading the 147 source, not from a trace, so please treat it as a lead
rather than a diagnosis:
- The lines come from the software display path:
DisplayResourceProviderSoftware::LockForRead
(display_resource_provider_software.cc:47-72) callsProduceMemorythe first time it draws a tile. A failure isn't
cached and the quad is skipped (:161-169). That would explain hundreds of lines per close: every frame drawn after
the images are gone logs once per tile. CefWindowX11::Close(window_x11.cc:203-218) sendsWM_DELETE_WINDOWand also hashost->Close()post
CloseNow. Those two race. When the X event is handled first,WindowDestroyedtears down the browser while the
window's compositor is still alive. The Chrome-style result is why we think this is the part that matters.HostFrameSinkManageronly waits for a window's display to be destroyed under GPU compositing
(host_frame_sink_manager.cc:205-207), so under software compositing nothing makes the display stop before the
renderer's images go. Chrome style runs under the same rule and stays clean, though, so we read this as the
condition that lets the race show rather than the race itself.
We haven't confirmed this with a trace, and we might be wrong about the order in the four-window case. If the reading
holds, these are the two changes we'd try. Neither is built or tested.
[suggestion] (1) Destroy the child host, and with it the compositor, before the browser in CEF's WM_DELETE_WINDOW
handler. This one is narrow and CEF-only, so it's the one we'd try first:
// libcef/browser/native/window_x11.cc, WM_DELETE_WINDOW handler, around :424
if (protocol == x11::GetAtom(kWMDeleteWindow)) {
// We have received a close message from the window manager.
if (!browser_ || browser_->TryCloseBrowser()) {
+ // Close() only posts CloseNow(), and this event can arrive first.
+ if (auto* host = GetHost()) {
+ host->CloseNow();
+ }
// Allow the close.
connection_->DestroyWindow({xwindow_});From our reading, CloseNow tears down the host and its ui::Compositor before the browser goes, which is the order
Chrome style seems to get, and it doesn't re-enter the window or the browser. If the posted CloseNow already ran,
GetHost() returns null, so nothing runs twice. Under software compositing the viz side of that teardown is still
asynchronous, which is what (2) is about.
[suggestion, broader] (2) If (1) makes it rarer but doesn't stop it, also wait for display destruction under
software compositing. We're guessing the wait is GPU-only today to keep a synchronous call off the UI thread when
there's no GPU context to protect, so this costs something, and it touches Chromium rather than CEF:
// components/viz/host/host_frame_sink_manager.cc, around :205
- // Only wait on destruction if using GPU compositing for the window.
- data.wait_on_destruction =
- maybe_wait_on_destruction && params->gpu_compositing;
+ data.wait_on_destruction =
+ maybe_wait_on_destruction &&
+ (params->gpu_compositing || BUILDFLAG(IS_LINUX));Happy to run a patched build or capture a gpu,viz trace if that's more useful than more guesses.
Assisted-by: Claude (repro harness and this write-up; we reviewed and ran everything above)
Activity
marcospaulo commentedon Nov 8, 2026
We captured a
viz,gpu,cctrace (CefBeginTracing/CefEndTracing) in the small SetAsChild program from the firstpost, and it narrows things down. One caveat first: tracing slows things enough that the error itself stops showing
(0 of 8 traced launches), so these traces show the order of events rather than the failure itself.
What the trace shows
that window's
Compositoris destroyed. In one close, the display drew three more frames afterCompositor::destructorfor that browser._NET_WM_STATE_HIDDENon a browser'sGetWindowHandle(), the page getsvisibilitychangeto hiddenabout 10 ms later, but
Display::SetVisible(false)only reaches viz 26 to 31 ms later. In the trace the viz threadis busy with software draws for the other windows at that point, so the hide seems to wait behind them.
XUnmapWindow) hides the page but never hides the display. Unmapping Chromium's ownchild window gave 122
ProduceMemorylines even under tracing: the pages were hidden and released their tiles, andthe displays went on drawing for the full 500 ms.
So it looks like the error happens whenever a page releases its tiles (hidden, navigated or closed) while its window's
display is still drawing. From reading the source (not traced), Chrome style hides the widget before the WebContents
goes, which would fit with it staying clean.
What we tried on the embedder side
Same program, four browsers, 50 launches each unless noted. For these runs the page also writes its
visibilityStateinto its title, which the program reads inOnTitleChange.CloseBrowser(true)on all four at once (the run from the first post)OnBeforeCloseabout:blank(closing onOnLoadEnd)_NET_WM_STATE_HIDDEN(closing on the page'svisibilitychange)_NET_WM_STATE_HIDDENat once, then closed one at a time (each after the previousOnBeforeClose) once every page reported hidden_NET_WM_STATE_HIDDENat once, closed 500 ms later (a timer, for comparison)Hiding every window first is what helps. Starting the closes on the pages'
visibilitychangegets close, but itisn't a guarantee: that notice comes 4 to 10 ms after the hide, and the displays stop 26 to 31 ms after it. As far as
we can tell, no notification an embedder can see comes after the display has stopped drawing, so the only clean order
we found needs a timer.
[question] Would CEF consider hiding the window's compositor, and waiting for that, before the browser's
WebContents is hidden or destroyed in the Alloy native close path? Or exposing a callback once the display has
stopped? Either would give embedders a safe order without guessing at a delay. Happy to share the program, run a
patched build, or send the traces (about 20 MB zstd).
Assisted-by: Claude (repro harness, traces and this write-up; we reviewed and ran everything above)
magreenblatt commentedon Nov 9, 2026
Please re-test the supplied cefsimple reproduction with CEF 156 or newer and report the exact version and failure rate.
If it still reproduces, please also attach the captured
viz,gpu,cctraces and corresponding stderr logs, particularly the unmapping run that produced errors under tracing. Those would help correlate display shutdown with the mailbox errors.