Monday, November 9, 2026

Blank GDM after Ubuntu 26.04 LTS upgrade: a stale bwrap AppArmor profile

TL;DR

After upgrading my laptop from Ubuntu 24.04 LTS to 26.04.1 LTS, the login widget failed and GDM came up as a blank canvas with a clock. Nothing on it would take a click or a keystroke, other than the clock bringing up a non-functional calendar.

The cause was an AppArmor profile I'd written by hand back in April to get the Codex sandbox working with bwrap on 24.04.

On 26.04 it fights with the bwrap profiles Ubuntu started shipping in 25.xx, bwrap loses its permissions, and GNOME can no longer load any image.

The fix: delete /etc/apparmor.d/usr.bin.bwrap, unload it, and reload AppArmor. Future me: check /etc/apparmor.d for hand-made profiles before the next dist-upgrade.

The symptom

The 26.04 LTS upgrade path opened on Sept 1, so I ran do-release-upgrade. It got stuck on a conffile prompt ("keep your modified Xorg config?") that timed out, and I had to clean up a half-configured apt afterwards. That's a red herring, but not a totally innocent one. Between 24.04 and 26.04, Ubuntu removed the Xorg session as an option for GNOME, moving all logins to Wayland. So I spent time looking for Wayland issues when it was ultimately blameless in this case.

After the reboot:

  • GDM showed the top bar and a clock, and nothing else: no user list, no password box.
  • Clicking the clock worked. Typing and clicking anywhere else did nothing.
  • Ctrl+Alt+F3 gave me a text console, so the box itself was fine.

How I got there

The usual suspects didn't pan out: no WaylandEnable=false in /etc/gdm3/custom.conf, no leftover xorg.conf, an Intel GPU (so no NVIDIA module fun), and AccountsService was running fine. The bugs were further down the stack.

1. The greeter's JavaScript was crashing. On 26.04 the greeter doesn't run as the gdm user (uid 127 on my system). It runs as a dynamic gdm-greeter user, so journalctl _UID=127 comes back empty. Find the real uid with ps and look there instead:

% ps -eo user,uid,cmd | grep gnome-shell
% journalctl -b _UID=60579
...
gnome-shell[46412]: JS ERROR: Error: Argument child may not be null
    _updateLogoTexture@resource:///org/gnome/shell/gdm/loginDialog.js:1086:27
    _updateLogo@resource:///org/gnome/shell/gdm/loginDialog.js:1094:14
    _init@resource:///org/gnome/shell/gdm/loginDialog.js:679:14

The login dialog dies while it loads the logo. The shell keeps running, so you get the top bar and nothing else. Setting logo='' in /etc/gdm3/greeter.dconf-defaults and running dpkg-reconfigure gdm3 got me a working login prompt. Every icon in the session was still blank, though, and gnome-terminal crashed on launch.

2. No image loaded anywhere. GNOME 49+ decodes images through glycin, which runs every loader inside a bwrap sandbox. A one-liner test:

% python3 -c "import gi;gi.require_version('GdkPixbuf','2.0');from gi.repository import GdkPixbuf as G;print(G.Pixbuf.new_from_file('/usr/share/backgrounds/warty-final-ubuntu.png'))"
gi.repository.GLib.GError: ... Loader process exited early with status '1'Command:
 "bwrap" "--unshare-all" ... "/usr/libexec/glycin-loaders/2+/glycin-image-rs"

So the loaders were installed and fine, but bwrap was failing.

3. AppArmor was denying bwrap:

% sudo journalctl -b -k | grep DENIED
apparmor="DENIED" operation="capable" profile="unprivileged_userns" comm="bwrap" capname="setpcap"
apparmor="DENIED" operation="capable" profile="unprivileged_userns" comm="bwrap" capname="net_admin"

bwrap was running under the catch-all unprivileged_userns profile instead of a bwrap profile. Then aa-status showed two profiles loaded for the same binary: bwrap (from Ubuntu's bwrap-userns-restrict) and /usr/bin/bwrap. dpkg -S said no package owned the second one.

Root cause

The orphan was mine:

% cat /etc/apparmor.d/usr.bin.bwrap
abi <abi/4.0>,
include <tunables/global>

/usr/bin/bwrap flags=(unconfined) {
  userns,
}

Back in April, on 24.04, Codex's sandbox (which uses bwrap) failed because Ubuntu restricts unprivileged user namespaces. The common fix online, for example this Codex-on-24.04 post, is a tiny unconfined profile that grants bwrap the userns permission. I wrote my own variant under the old path-style filename. It worked, and I forgot about it.

On 26.04 the apparmor package ships its own bwrap-userns-restrict profile, plus glycin.bwrap and glycin.loaders for GNOME's image sandbox, and they all claim /usr/bin/bwrap. AppArmor didn't "apply no rules." With two profiles fighting over one binary, it logs a conflicting-attachment error and falls back to the restrictive unprivileged_userns profile. That profile denies setpcap and net_admin, so bwrap can't set up the sandbox and every glycin loader dies.

The resulting chain:

  1. The leftover profile collides with the profiles 26.04 ships.
  2. bwrap lands in unprivileged_userns and can't start its sandbox.
  3. Glycin can't decode any image.
  4. The GDM login dialog throws on its logo, and you get a blank greeter.
  5. In the session, every icon is blank and gnome-terminal aborts on its "image-missing" icon.

I'm not the only one. The claude-desktop-debian project hit the exact same chain from a stale bwrap profile its old installer left behind: blank GDM, gnome-terminal crashing, broken keyring prompts.

The fix

# find profiles no package owns
% for f in /etc/apparmor.d/*; do [ -f "$f" ] && ! dpkg -S "$f" >/dev/null 2>&1 && echo "$f"; done
/etc/apparmor.d/usr.bin.bwrap

# unload it from the kernel, then move it out of the way
% sudo apparmor_parser -R /etc/apparmor.d/usr.bin.bwrap
% sudo mkdir -p /root/apparmor-disabled
% sudo mv /etc/apparmor.d/usr.bin.bwrap /root/apparmor-disabled/
% sudo systemctl reload apparmor

# verify: images load again
% python3 -c "import gi;gi.require_version('GdkPixbuf','2.0');from gi.repository import GdkPixbuf as G;print(G.Pixbuf.new_from_file('/usr/share/backgrounds/warty-final-ubuntu.png'))"
<GdkPixbuf.Pixbuf object at 0x77b775707640 (GdkPixbuf at 0x3a928900)>

The apparmor_parser -R step matters. Deleting the file alone leaves the profile loaded in the kernel until the next reboot.

Log out and back in, and icons, wallpaper and gnome-terminal are back. You can also undo the logo='' workaround. Ubuntu's real default comes from ubuntu-settings (/usr/share/pixmaps/ubuntu-logo-text-dark.svg), and the file was there all along; it just couldn't be decoded. The commented /usr/share/images/vendor-logos/... path in greeter.dconf-defaults is a Debian leftover that doesn't exist on Ubuntu.

Lessons

  • Hand-made files in /etc/apparmor.d are landmines across releases. Run the dpkg -S loop above before every dist-upgrade.
  • I actually have to learn AppArmor now. It's no longer just a server thing. Ubuntu uses it to gate user namespaces, which means sandboxes, which means even GNOME's image loading. Pasting a profile from a blog post without understanding it is how I got here.
  • Don't "fix" this by overwriting bwrap-userns-restrict. Some guides (e.g. nanobot#4236) suggest replacing it with an unconfined profile. On 26.04 that file belongs to the apparmor package, so you'd recreate the conflict and earn another conffile prompt next upgrade. dpkg --verify apparmor should print nothing.
  • If Codex or another bwrap tool breaks on 26.04, try the shipped profiles first. Read the new DENIED lines before adding anything.
  • "Blank GDM with a clock" is a JS crash in the greeter. Find the greeter's dynamic uid and grep its journal for JS ERROR.

Related links

No comments: