Public bug reported:

[ Impact ]

Bug Description:

If the monitor configuration changes while gnome-shell is loading the
desktop backgrounds at login, gnome-shell never finishes starting up.
The startup animation never runs and 'startup-complete' is never
emitted. A full-screen, input-blocking cover pane stays on top, so the
session ignores mouse input, and depending on how far startup got, the
user may see a blank/gray screen or a desktop that looks normal but
cannot be used. The session never recovers by itself, so gnome-shell has
to be restarted.

Who Is Affected:

Ubuntu GNOME sessions on Noble (GNOME 46) and Jammy (GNOME 42) whose
monitor layout changes in the first few seconds of the session. This is
common in virtual machines and remote or cloud desktops, where the
client resizes the guest display right after it connects. It can also
happen with monitor hotplug, docking stations or displays that change
mode during login, and with extensions that reload the backgrounds
during startup (upstream GNOME issue #9018, seen on both VMs and bare
metal).

Root Cause:

In js/ui/layout.js, LayoutManager._prepareStartupAnimation() runs 'await
this._updateBackgrounds()', which waits until every BackgroundManager it
creates emits 'loaded'. If 'monitors-changed' fires before then,
_monitorsChanged() calls _updateBackgrounds() again. That destroys the
managers the startup code is waiting on and disconnects their 'loaded'
handlers, so the first promise never settles. As a result, 'startup-
prepared' and 'startup-complete' are never emitted, the startup
animation never runs, and the input-blocking cover pane is never
removed. Jammy's js/ui/layout.js contains the same code (see Other
Info).

Fix:

Three upstream commits from GNOME MR !4070, which have been upstream
since GNOME 50.rc. The first, "layout: Catch async errors", makes sure
failures from _updateBackgrounds() are logged instead of silently
dropped. The second, "layout: Reject promise if background loading is
interrupted", guards each wait with a Gio.Cancellable. A newer
_updateBackgrounds() call cancels it, so the stale wait is rejected
instead of hanging forever. The third, "layout: Wait for backgrounds to
load, even when interrupted", makes all callers share one promise that
resolves only when a set of backgrounds finishes loading without
interruption, so startup still waits for a real wallpaper as it did
before.

One small Ubuntu-specific patch is added on top. The upstream code uses
Promise.withResolvers(), which does not exist in the GJS shipped in
Noble (1.80, SpiderMonkey 115) or Jammy (1.72, SpiderMonkey 91), since
it first appeared in Firefox 121. The patch replaces both uses with the
equivalent "new Promise((resolve, reject) => ...)" construction, so
behavior is identical. Without it, the backport fails at runtime with
"TypeError: Promise.withResolvers is not a function" and startup hangs
on every login.

SRU Justification:

The failure makes the session unusable and gives the user no way to
recover. It happens every time the trigger occurs, and the trigger is
routine for virtual machines and remote desktops, whose display is
resized as the client connects. The fix is small, confined to two
functions in one file, already upstream, and has been verified on both
Noble and Jammy with the automated reproducer described below.

[ Test Plan ]

An autopkgtest is not plausible for this upload. The gnome-shell source
package has no debian/tests, and its build-time tests deliberately skip
the "shell" suite (debian/rules passes --no-suite shell) because full-
shell tests are not reliable on the buildds. This bug also needs a
complete gnome-shell instance with a virtual monitor that can be changed
at runtime. Instead, a self-contained reproducer is attached to this
bug. It runs in a clean LXD VM of the target release: on Noble it uses
gnome-shell's own headless shell-test infrastructure, and on Jammy it
runs the same test steps inside the packaged gnome-shell (see
"Differences Between the Noble and Jammy Tests" below).

Reproducer Contents:

startupBackgroundRace.js is the Noble test, which is derived from
tests/shell/startupMonitorChanges.js in upstream MR !2763. bg-race@test
is the same test on Jammy, packaged as a GNOME Shell extension. setup-
vm.sh, which enables deb-src, runs "apt-get source gnome-shell",
installs the dependencies and prepares the unpatched "stock" variant.
build-variant.sh makes a copy of the source with the given patches
applied. run-bg-race.sh runs each variant N times in "control" mode (no
monitor change) and "trigger" mode (monitor change), and headless-vm-
repro.txt contains the full step-by-step instructions.

How the Test Works:

gnome-shell starts headless with the wallpaper pointed at a named pipe
(FIFO) via SHELL_BACKGROUND_IMAGE, so background loading blocks until
the test writes the image into the pipe. The test waits until
_prepareStartupAnimation() is waiting on the backgrounds. In trigger
mode, it then changes the monitor configuration, which makes mutter emit
'monitors-changed' just as a remote client resizing the session would.
Finally it writes the wallpaper into the pipe. The run passes if
'startup-complete' fires. It is reported as TIMEOUT only if the test
reached the point where startup was waiting on the backgrounds and
'startup-complete' then never fired (on Noble, meson's 60-second test
timeout expired; on Jammy, 'startup-complete' had not fired 30 seconds
after the wallpaper was released). Any other outcome, such as gnome-
shell failing to start or the test hitting an error before that point,
is reported as FAIL so it cannot be mistaken for the bug. Because the
pipe holds the race window open for as long as the test needs, the
result is deterministic rather than timing-dependent.

Steps:

On any amd64 host with LXD, create a VM of the release under test, copy
the harness and the patches from the upload into it, and run the stock
and fixed variants. For Jammy, use ubuntu:22.04 in the launch command
and "Jammy" (lowercase) in place of "Noble" in the build-variant.sh
command. setup-vm.sh detects the release by itself.

  lxc launch ubuntu:24.04 gnome-headless --vm -c limits.cpu=6 -c 
limits.memory=6GiB -d root,size=20GiB
  lxc file push -p setup-vm.sh build-variant.sh run-bg-race.sh 
startupBackgroundRace.js gnome-headless/root/headless-test/
  lxc file push -r -p bg-race@test gnome-headless/root/headless-test/Jammy/
  lxc file push -p 0001-layout-Catch-async-errors.patch 
gnome-headless/root/patches/
  lxc file push -p 
0002-layout-Reject-promise-if-background-loading-is-inter.patch 
gnome-headless/root/patches/
  lxc file push -p 
0003-layout-Wait-for-backgrounds-to-load-even-when-interr.patch 
gnome-headless/root/patches/
  lxc file push -p 
0004-layout-Adapt-background-loading-fix-for-GNOME-46-GJS.patch 
gnome-headless/root/patches/
  lxc shell gnome-headless
  chmod +x /root/headless-test/*.sh
  /root/headless-test/setup-vm.sh
  cd /root/headless-test
  ./build-variant.sh Noble fixed 
/root/patches/0001-layout-Catch-async-errors.patch 
/root/patches/0002-layout-Reject-promise-if-background-loading-is-inter.patch 
/root/patches/0003-layout-Wait-for-backgrounds-to-load-even-when-interr.patch 
/root/patches/0004-layout-Adapt-background-loading-fix-for-GNOME-46-GJS.patch
  ./run-bg-race.sh 3 stock fixed
  grep -E 'BG-RACE|SIGTERM' /root/bg-race/stock-trigger1-run1.testlog.txt
  grep BG-RACE /root/bg-race/fixed-trigger1-run1.testlog.txt

Log Locations:

Every run keeps gnome-shell's full output in /root/bg-
race/<variant>-trigger<0|1>-run<N>.testlog.txt inside the VM, for
example /root/bg-race/stock-trigger1-run1.testlog.txt for the first
stock trigger run. On Noble this is meson's test log, which also records
the test result and duration. The test's own messages in these logs
start with "BG-RACE:", so grepping for that prefix shows what happened
during startup. The PASS/TIMEOUT/FAIL summary printed by run-bg-race.sh
is also saved to /root/bg-race/summary.txt. The logs, not the summary,
are the evidence: they show directly that the monitor change happened
while startup was waiting on the backgrounds, and whether 'startup-
complete' followed.

Expected Results:

The stock variant passes every control run and times out on every
trigger run, which shows the bug. The fixed variant passes every run in
both modes.

In the stock trigger log, the BG-RACE lines show the startup animation
waiting for backgrounds, the monitor layout changing while the
backgrounds were loading, and the wallpaper being released, but no
"startup-complete emitted" line follows. On Noble the log then ends with
meson killing the shell ("killed by signal 15 SIGTERM") after 60
seconds. An "Xwayland exited unexpectedly" message at the very end comes
from that kill and is not part of the bug. On Jammy the last BG-RACE
line is "startup-complete not emitted within 30 s":

  BG-RACE: startup animation is waiting for backgrounds
  BG-RACE: monitor layout changed while backgrounds were loading
  BG-RACE: releasing wallpaper load
  (no "startup-complete emitted" Noble: killed by signal 15 SIGTERM, Jammy: 
startup-complete not emitted within 30 s)

In the fixed trigger log, the same sequence is followed by startup
completing, about a second after the wallpaper is released:

  BG-RACE: startup animation is waiting for backgrounds
  BG-RACE: monitor layout changed while backgrounds were loading
  BG-RACE: releasing wallpaper load
  BG-RACE: startup-complete emitted
  BG-RACE: PASS

On Jammy, both trigger logs also contain a "changed Meta-0 1920x1080@60
to scale 2" line just before the monitor layout change, which records
the exact change the test made.

Results Obtained Before Upload:

On Noble (gnome-shell 46.0-0ubuntu6~24.04.17, mutter 46.2), stock passed
3/3 control runs and timed out on 3/3 trigger runs, and fixed passed 3/3
runs in both modes. On Jammy (gnome-shell 42.9-0ubuntu2.4, mutter
42.9-0ubuntu9, GJS 1.72.4), the results were identical. On both
releases, a variant with only the three upstream commits timed out in
both modes and logged "TypeError: Promise.withResolvers is not a
function", which shows why the GJS adaptation patch is required. The
same four patches apply to both releases and produce the same code on
each. Both reproducers were rerun with the final four patches, with 0002
as corrected under Deviation From Upstream. build-variant.sh applies
patches the same strict way dpkg-source does when building the package,
so a patch with non-matching context stops the build. The results above
are from those runs.

Verification of -proposed:

The reproducer works from source. To check the -proposed upload, enable
deb-src for -proposed in the VM and re-run setup-vm.sh with
GNOME_SHELL_VERSION set to the -proposed version (on Jammy this also
installs that version of the gnome-shell package). The "stock" variant
is then the SRU candidate, and it must pass in trigger mode.

Differences Between the Noble and Jammy Tests:

Both tests follow the same steps: block the wallpaper load with a FIFO,
wait until _prepareStartupAnimation() is waiting on
_updateBackgrounds(), change the monitor configuration (trigger mode
only), release the wallpaper, and pass only if 'startup-complete' fires.
What differs is how the test gets into gnome-shell, how it changes the
monitors, and what code it runs, because GNOME 42 lacks the test
infrastructure the Noble test relies on.

On Noble, there is a shell test registered in tests/meson.build and run
by gnome-shell-test-tool with --automation-script. GNOME 42.9 has no
tests/shell directory, no gnome-shell-test-tool and no --automation-
script option, and its only scripting hook (SHELL_PERF_MODULE) starts
after 'startup-complete', which is too late to see the hang. On Jammy
the test is therefore a GNOME Shell extension. Extensions are enabled
synchronously during Main.start(), before the startup animation is
prepared, so the extension observes the race from the same point as the
Noble test.

The trigger connects a virtual test monitor (MetaTest.TestMonitor) and
swaps it for one of a different size (from 1280x720 to 1440x900.) That
test monitor comes from mutter's newer test library (mutter-14-tests)
and does not exist in mutter 42. On Jammy, gnome-shell instead starts
with a fixed 1920x1080 virtual monitor, and the trigger changes its
scale from 1 to 2 through
org.gnome.Mutter.DisplayConfig.ApplyMonitorsConfig. This is the simplest
change mutter 42 accepts for a headless virtual monitor, and it makes
mutter emit a real 'monitors-changed' that reaches _monitorsChanged()
and _updateBackgrounds() by the same path as a resize or hotplug. The
bug does not depend on what kind of monitor change happens, only on when
it happens.

Each variant is a full meson build of the gnome-shell source with the
patches applied, and the test runs that build. Without the shell-test
infrastructure there is no supported way to run an uninstalled 42.9
build, so on Jammy the packaged gnome-shell binary runs and each
variant's patched js/ui directory is loaded over the packaged JavaScript
with G_RESOURCE_OVERLAYS. The fix only changes js/ui/layout.js, so this
is enough to test it. To keep it valid, build-variant.sh refuses Jammy
patches that touch files outside js/ui/, and refuses to run if the
downloaded source version differs from the installed package. The
overlay was checked with a patch that adds a log line to
_prepareStartupAnimation(), and the line appeared in both modes.

[ Where problems could occur ]

Startup Timing:

The patch changes the code path every session uses to decide when the
startup animation can begin. The new code resolves the shared promise
only after an uninterrupted successful load of all backgrounds. If some
situation keeps interrupting background loading during startup, or a
background never emits 'loaded' (for example an XML slideshow whose
files fail in an unexpected way), startup could wait longer than before
or never finish, which would look like the same stuck login this SRU
fixes. A mistake in the other direction would let the startup animation
run before the wallpaper is ready, showing the plain background color
during the animation.

Extensions Using Private Methods:

_updateBackgrounds() is now an async function that returns a promise
shared across calls, and _waitLoaded() takes an extra cancellable
argument. gnome-shell itself only calls these from layout.js, but third-
party extensions that call or override these private methods could
break, for example an extension that replaces _updateBackgrounds() or
calls _waitLoaded() with one argument. That would show up as JS errors
from the extension, or as backgrounds not being drawn.

Monitor Changes After Startup:

Monitor hotplug and resolution changes after startup also go through
_updateBackgrounds(), and the cancellable is now cancelled and replaced
on every call. A mistake here could leave backgrounds missing or stale
on one monitor after a hotplug, or leave signal handlers connected,
causing a small leak. The caller in _monitorsChanged() now uses
.catch(logError), so unexpected failures are logged in the journal
instead of silently ignored.

GJS Adaptation Patch:

The patch that replaces Promise.withResolvers() is new code that is not
upstream, and it is used on both Noble and Jammy. If it were wrong,
startup would fail on every login, not only after a monitor change. The
reproducer's control mode covers this case and passes on both releases.

[ Other Info ]

Upstream References:

The upstream bug is GNOME issue #9018
(https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/9018), and the fix
is GNOME MR !4070 (https://gitlab.gnome.org/GNOME/gnome-
shell/-/merge_requests/4070), first released in GNOME 50.rc. The three
commits are:

  c89d2061b38aa69f3aaafeb594d598c07b14721b  layout: Catch async errors
  ac5f521c656d4f88e1b62c2e7e1d610850515ca9  layout: Reject promise if 
background loading is interrupted
  0575c6b5745ad84890409a82930705efec71d2d8  layout: Wait for backgrounds to 
load, even when interrupted

A related, still open MR, https://gitlab.gnome.org/GNOME/gnome-
shell/-/merge_requests/2763, contains an earlier fix for the same issue
and the startupMonitorChanges.js shell test that the attached reproducer
is derived from. There is also a downstream Fedora report:
https://bugzilla.redhat.com/show_bug.cgi?id=2437673

Deviation From Upstream:

Two changes were made compared to a plain cherry-pick of the three
upstream commits.

First, one context line in 0002-layout-Reject-promise-if-background-
loading-is-inter.patch was changed from "const bgManager =
this._createBackgroundManager(i);" to "let bgManager =
this._createBackgroundManager(i);". It is an unchanged line next to the
added code, not a line the commit modifies. Upstream's main branch has
"const" there, but the Ubuntu sources for both Noble 46.0 and Jammy 42.9
have "let". Package builds (dpkg-source) refuse to apply a patch whose
context does not match exactly, and without this change the build stops
at that patch on both releases. The added and removed lines are exactly
the upstream ones, so the resulting code is unchanged. The other two
upstream commits are applied verbatim.

Second, the extra 0004-layout-Adapt-background-loading-fix-for-
GNOME-46-GJS.patch is needed only because the GJS in Noble and Jammy
lacks Promise.withResolvers(). It is kept as a separate patch so the
upstream commits stay unchanged and the adaptation is easy to review.

The same four patches are used for Noble and Jammy, and the resulting
code is the same on both. The GJS adaptation patch is needed on Jammy
too, since GJS 1.72 (SpiderMonkey 91) also lacks
Promise.withResolvers(). The other language features the fix uses
(optional chaining, optional catch binding and
Gio.Cancellable.connect()) all work on GJS 1.72, and the fixed variant
passes in both modes.

Relationship to LP #2019751 and Upstream MR !2763:

LP #2019751 (https://bugs.launchpad.net/ubuntu/+source/gnome-
shell/+bug/2019751, "Desktop area may get clipped to a small rectangle
if the hypervisor changes screen resolution during the login animation")
covers the same family of failure: the login animation never completes
after a display change. It tracks upstream GNOME issue #6473
(https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/6473). It has Jammy
and Noble tasks (Triaged and Confirmed), but gnome-shell itself was
never fixed there. The only change released was a workaround in ubuntu-
dock, and the bug is still waiting on upstream MR !2763
(https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2763).

This report is for the specific hang described under the Impact section,
the startup animation waits forever for background managers that a later
monitor change destroys. !2763 contains a fix for that same hang (its
commit "layout: Reject loading background promise if monitors had
changed" describes the same cause). With the relevant !2763 commits
ported to the Noble 46.0 source, the reproducer attached to this bug
passes (control and trigger, 3/3 each). However, !2763 is a much larger
change than the fix in this upload:

  - !2763: 11 commits, 7 files, +326/-47 lines, of which about 180 lines are a 
new shell test. It also changes the cover pane and clip handling, restarts the 
startup animation preparation, relayouts the UI actor on stage size changes, 
and changes background.js, main.js, overviewControls.js and scripting.js.
  - !4070 (this upload): 3 commits, 1 file (js/ui/layout.js), +33/-9 lines.

The three upstream commits in this upload come from !4070 (GNOME issue
#9018, https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/9018). That
MR has been merged upstream and was released in GNOME 50.rc, while !2763
is still open and has not been merged. The cause described in #9018 is
the same one (a second _updateBackgrounds() call destroys the managers
the startup code is waiting on). There, Fedora 44 beta users were
affected, and Fedora backported !4070 to its builds.

The scope is deliberately narrow. This upload does not change the cover
pane or clip handling that !2763 also updates. I have not tested whether
that part is needed for the clipped-desktop symptom in LP #2019751, and
the reproducer does not check it. The bug in this report has also
already affected a user, the original report came from an Ubuntu desktop
on Amazon WorkSpaces using Amazon DCV, where the session showed a gray
screen because DCV changes the display layout as the client connects.

** Affects: gnome-shell (Ubuntu)
     Importance: High
     Assignee: Dean Marx (dmarx11)
         Status: New

** Affects: gnome-shell (Ubuntu Jammy)
     Importance: High
     Assignee: Dean Marx (dmarx11)
         Status: New

** Affects: gnome-shell (Ubuntu Noble)
     Importance: High
     Assignee: Dean Marx (dmarx11)
         Status: New

-- 
You received this bug notification because you are a member of Ubuntu
Bugs, which is subscribed to Ubuntu.
https://bugs.launchpad.net/bugs/2173589

Title:
  gnome-shell startup stuck forever when monitor layout changes during
  login

To manage notifications about this bug go to:
https://bugs.launchpad.net/ubuntu/+source/gnome-shell/+bug/2173589/+subscriptions


-- 
ubuntu-bugs mailing list
[email protected]
https://lists.ubuntu.com/mailman/listinfo/ubuntu-bugs

Reply via email to