Skip to content

Linux: closing several Alloy browsers at once under software compositing logs "ProduceMemory ... non-existent mailbox" errorsΒ #4351

Open

Description

@marcospaulo

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.

  1. Save page3.html (below) and serve it from 127.0.0.1, e.g. python3 -m http.server 18797 --bind 127.0.0.1.
  2. 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-sandbox is only there because chrome-sandbox isn't setuid in our unpacked copy.
  3. 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.
  4. 1.5 to 3.5 s later, send WM_DELETE_WINDOW to 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.
  5. Look for ProduceMemory on 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 $XP

Results, 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 with SystemInfo.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) calls ProduceMemory the 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) sends WM_DELETE_WINDOW and also has host->Close() post
    CloseNow. Those two race. When the X event is handled first, WindowDestroyed tears 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.
  • HostFrameSinkManager only 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

  1. marcospaulo commented on Nov 8, 2026

    @marcospaulo
    Author

    We captured a viz,gpu,cc trace (CefBeginTracing / CefEndTracing) in the small SetAsChild program from the first
    post, 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

    • When a native Alloy browser closes, its window's display keeps drawing the page's last frame until about the time
      that window's Compositor is destroyed. In one close, the display drew three more frames after
      Compositor::destructor for that browser.
    • When we set _NET_WM_STATE_HIDDEN on a browser's GetWindowHandle(), the page gets visibilitychange to hidden
      about 10 ms later, but Display::SetVisible(false) only reaches viz 26 to 31 ms later. In the trace the viz thread
      is busy with software draws for the other windows at that point, so the hide seems to wait behind them.
    • Unmapping the window instead (XUnmapWindow) hides the page but never hides the display. Unmapping Chromium's own
      child window gave 122 ProduceMemory lines even under tracing: the pages were hidden and released their tiles, and
      the 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
    visibilityState into its title, which the program reads in OnTitleChange.

    Close order at quit Launches with the error
    CloseBrowser(true) on all four at once (the run from the first post) 9 / 25
    One at a time, each after the previous OnBeforeClose 20 / 50
    One at a time, each first loading about:blank (closing on OnLoadEnd) 19 / 50
    One at a time, each first set _NET_WM_STATE_HIDDEN (closing on the page's visibilitychange) 17 / 50
    All four set _NET_WM_STATE_HIDDEN at once, then closed one at a time (each after the previous OnBeforeClose) once every page reported hidden 1 / 50
    All four set _NET_WM_STATE_HIDDEN at once, closed 500 ms later (a timer, for comparison) 0 / 50

    Hiding every window first is what helps. Starting the closes on the pages' visibilitychange gets close, but it
    isn'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)

  2. magreenblatt commented on Nov 9, 2026

    @magreenblatt
    Collaborator

    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,cc traces and corresponding stderr logs, particularly the unmapping run that produced errors under tracing. Those would help correlate display shutdown with the mailbox errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    alloy-styleRelated to Chrome runtime + Alloy stylelinuxLinux platformneeds user feedbackAdditional feedback required

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions