How Chrome's font cache semi-bricked my KDE desktop
Chrome 154 replaced the system's fontconfig cache files with symlinks to its own, and every new Qt app started segfaulting. How I found it, proved it in a sandbox, and keep it away with a systemd path unit until Chrome ships a fix.
In this post 12 sections
On a Wednesday morning my laptop lost its desktop. The Plasma panel disappeared, a new Konsole window crashed, and krunner went the same way. Three reboots changed nothing: Plasma started, segfaulted within a second, and systemd gave up on it.
Nothing KDE-related had been updated. The cause was the browser, which never crashed once.
The setup
A ThinkPad T14s with an AMD Radeon 890M, running Ubuntu 24.04.5, KDE Plasma 5.27.11 on X11 and a mainline 6.18.1 kernel. The system fontconfig is Ubuntu’s 2.15.0. Chrome is Google’s own google-chrome-stable package.
A crash loop with no crash dialog
plasmashell runs as a systemd user service, and the journal showed the same pattern after every login:
plasmashell[3992]: KCrash: crashing... crashRecursionCounter = 2
plasmashell[3992]: Unable to start Dr. Konqi
systemd[2523]: plasma-plasmashell.service: Main process exited, code=dumped, status=11/SEGV
systemd[2523]: plasma-plasmashell.service: Start request repeated too quickly.
systemd[2523]: Failed to start plasma-plasmashell.service - KDE Plasma Workspace.A recursion counter of 2 means the crash handler crashed as well, so DrKonqi never appeared to say what had happened. After three quick restarts systemd stops trying, and what’s left is open windows with no panel and no desktop behind them.
Nothing in the upgrade touched KDE
The first crash was at 09:57. An apt upgrade had run at 09:13, so that was the first place to look:
| Package | Change |
|---|---|
| google-chrome-stable | 153.0.8010.52 → 154.0.8037.57 |
| linux-generic and friends | 6.8.0-139 → 6.8.0-142, not the kernel I boot |
| sudo | 1.9.15p5-3ubuntu5.24.04.2 → .3 |
No KDE, Qt, Mesa or fontconfig packages. The running kernel was the same mainline 6.18.1 that had been fine for days. Nothing pointed at Chrome either, because Chrome wasn’t the thing crashing.
The backtrace points at fontconfig
Ubuntu’s apport keeps a core dump of every crash in /var/crash, and gdb can read it without debug symbols:
apport-unpack /var/crash/_usr_bin_plasmashell.1000.crash plasmashell-crash
gdb -batch -ex "bt 15" /usr/bin/plasmashell plasmashell-crash/CoreDump#4 <signal handler called>
#5 ?? () libfontconfig.so.1
#6 FcCharSetHasChar () libfontconfig.so.1
#7 ?? () libQt5XcbQpa.so.5
#8 QFontEngineMulti::stringToCMap(...) libQt5Gui.so.5
#9 QTextEngine::shapeText(int) libQt5Gui.so.5
#10 QTextEngine::boundingBox(int, int) libQt5Gui.so.5
#11 QFontMetricsF::boundingRect(QString) libQt5Gui.so.5
#12 ... libQt5Quick.so.5Qt is laying out a piece of text, hits a character the current font doesn’t have, and asks fontconfig which fallback font covers it. fontconfig segfaults while answering. Every Qt application goes through that code, which is why Konsole and krunner died the same way as plasmashell.
fontconfig itself hadn’t changed, so the data it reads probably had. For fontconfig, that means the cache.
Something rewrote the font cache
This is one font directory’s entry in ~/.cache/fontconfig, trimmed to the permissions and names:
lrwxrwxrwx 2300eef321c393bfd76478a5c0e95b23-le64.cache-10 -> 2300eef321c393bfd76478a5c0e95b23-le64.cache-12
lrwxrwxrwx 2300eef321c393bfd76478a5c0e95b23-le64.cache-11 -> 2300eef321c393bfd76478a5c0e95b23-le64.cache-12
-rw-rw-r-- 2300eef321c393bfd76478a5c0e95b23-le64.cache-12
-rw-rw-r-- 2300eef321c393bfd76478a5c0e95b23-le64.cache-7
lrwxrwxrwx 2300eef321c393bfd76478a5c0e95b23-le64.cache-9 -> 2300eef321c393bfd76478a5c0e95b23-le64.cache-12
-rw-rw-r-- 2300eef321c393bfd76478a5c0e95b23-le64.cache-reindex1-10The number at the end is the cache format, and Ubuntu’s fontconfig 2.15 reads format 9. Something had written 37 format-12 files at 09:14 and turned the format-9, 10 and 11 names into symlinks to them: 110 symlinks, all in the same minute. The file header agrees:
$ od -A n -t x4 -N 8 2300eef321c393bfd76478a5c0e95b23-le64.cache-12
fc02fc04 0000000cThat’s the magic number, then version 12. fontconfig 2.15 opens cache-9, follows the symlink, and uses what it finds.
Proving the cache was misread
fontconfig respects XDG_CACHE_HOME, so the same queries can run against the broken cache and against an empty directory, where it builds a fresh one:
| Query | Broken cache | Fresh cache |
|---|---|---|
fc-match emoji |
KaTeX_AMS-Regular.woff |
NotoColorEmoji.ttf |
.woff fonts without a family name |
20 | 0 |
With the broken cache, the best emoji font on the system was a KaTeX math font, flagged as a colour font, and twenty WOFF files had lost their names. Font fallback, which is where the crash happened, works from these same answers.
One red herring on the way: Firefox’s bundled TwemojiMozilla.ttf had been indexed at 09:39. A new emoji font showing up shortly before a crash in font fallback looked like a good lead, but a fresh cache handles it without complaint.
ldd said no, strings said yes
The first cache-12 file was written at 09:14:32. That’s the second Chrome 154 started for the first time, a minute after it was installed:
09:13:32 dpkg: upgrade google-chrome-stable 153.0.8010.52-1 154.0.8037.57-1
09:14:32 systemd: Started app-google\x2dchrome-….scope - Google Chrome - Web Browser.
09:14:32 first *-le64.cache-12 file written in ~/.cache/fontconfigA browser writing a cache format the system doesn’t use sounded unlikely, and ldd seemed to agree: Chrome links the system’s libfontconfig.so.1. strings found fontconfig inside the binary as well:
$ strings -n 8 /opt/google/chrome/chrome | grep -E 'Fontconfig error|/var/cache/fontconfig|conf\.avail' | head -3
Fontconfig error: out of memory
/usr/share/fontconfig/conf.avail
/var/cache/fontconfigThose are fontconfig’s internal messages and paths, and the binary also holds a built-in default fonts.conf whose cache directories are /var/cache/fontconfig and $XDG_CACHE_HOME/fontconfig. Chrome carries its own, newer fontconfig.
To rule out anything else on the machine, I ran Chrome 154 in a sandbox with an empty cache directory and a throwaway profile:
mkdir -p /tmp/repro/cache /tmp/repro/profile
echo '<p style="font-family:sans-serif">Hello 😀 中文</p>' > /tmp/repro/t.html
XDG_CACHE_HOME=/tmp/repro/cache google-chrome --headless=new --disable-gpu --no-first-run \
--user-data-dir=/tmp/repro/profile --screenshot=/tmp/repro/s.png file:///tmp/repro/t.html
ls -l /tmp/repro/cache/fontconfigThe result was 37 cache-12 files and 111 symlinks, the same pattern as that morning. Pointing the system fontconfig at the sandbox gave the same wrong answers: KaTeX for emoji, twenty nameless WOFF entries.
That also explains the 40 minutes between Chrome starting and the first crash. Everything already running had read the cache before 09:14. The first crash was a new Konsole window at 09:57.
Fixing it, with a detour through GLX
The fix is to delete the symlinks and let the system fontconfig write its own files again. Leave Chrome’s cache-12 files where they are. One of the other people who reported this moved the whole cache aside, and the symlinks were back the next time Chrome started. With the cache-12 files still in place, my next Chrome start didn’t recreate any.
cp -a ~/.cache/fontconfig ~/.cache/fontconfig.bak
find ~/.cache/fontconfig -maxdepth 1 -type l -delete
fc-cache -f
systemctl --user reset-failed plasma-plasmashell.service
systemctl --user start plasma-plasmashell.serviceplasmashell stopped crashing and started hanging instead, until systemd killed it at its 40-second start timeout. gdb on a fresh instance showed where it waited:
#6 xcb_wait_for_reply64 () libxcb.so.1
#7 _XReply () libX11.so.6
#12 glXChooseFBConfig () libGLX.so.0
#16 QOpenGLContext::create() libQt5Gui.so.5The X server answered normal requests, but glxinfo hung as well. The Xorg log had the explanation:
(II) AIGLX: Suspending AIGLX clients for VT switchI was working from a text console on tty3, because the desktop on tty2 was no use. While its VT isn’t active, Xorg suspends every GLX client, and plasmashell creates an OpenGL context at startup. That’s intended, and it makes a text console a bad place to test a desktop from. A transient unit that starts plasmashell once the X session is active again solved it:
systemd-run --user --unit=plasmashell-on-return /bin/sh -c '
while [ "$(loginctl show-session 3 -p Active --value)" != yes ]; do sleep 2; done
sleep 3
systemctl --user reset-failed plasma-plasmashell.service
systemctl --user start plasma-plasmashell.service'Session 3 was the X session here; loginctl list-sessions shows yours. Back on tty2 the panel came up on its own, and after one logout every KDE process was running against the rebuilt cache.
Why Chrome doesn’t get its own cache
This will come back. fontconfig caches are rebuilt when a font directory changes, which happens with every font package update and every font you install, and in the sandbox every cache Chrome wrote came with its three symlinks.
My first idea for keeping it away was a private cache for Chrome: point FONTCONFIG_FILE at a config that lists its own cache directory first and then includes the system config.
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<fontconfig>
<cachedir>~/.cache/fontconfig-chrome</cachedir>
<include ignore_missing="no">/etc/fonts/fonts.conf</include>
</fontconfig>For Chrome itself that works. In the sandbox all 148 of its cache entries landed in the private directory, the shared cache stayed empty, and the page still rendered emoji and CJK text. The problem is everything Chrome starts. Environment variables are inherited, so Okular opening a downloaded PDF would get Chrome’s FONTCONFIG_FILE too, and a system fontconfig reading that config finds Chrome’s symlinks:
$ FONTCONFIG_FILE=~/.config/google-chrome-fonts.conf fc-match emoji file
:file=/usr/share/fonts/truetype/katex/KaTeX\_AMS-Regular.woffThat moves the crash to wherever Chrome’s child processes end up, which is harder to trace than what I started with.
A path unit that undoes it
Instead, a systemd path unit watches the cache directory and repairs it the moment a symlink appears. It doesn’t matter how Chrome was started, and it would also catch a later version that does the same thing.
#!/bin/sh
# Chrome 154's bundled fontconfig replaces the system's cache-9 files with
# symlinks to its own cache-12 files, and Qt apps then segfault in
# FcCharSetHasChar. Remove the symlinks and rebuild.
dir="$HOME/.cache/fontconfig"
[ -d "$dir" ] || exit 0
n=$(find "$dir" -maxdepth 1 -type l -print -delete | wc -l)
[ "$n" -eq 0 ] && exit 0
echo "removed $n symlinks from $dir, rebuilding cache"
fc-cache -f
notify-send -a "Font cache guard" -i dialog-information \
"Font cache repaired" \
"Chrome replaced $n fontconfig cache files with symlinks again. They were removed and the cache was rebuilt." 2>/dev/null || true[Unit]
Description=Watch ~/.cache/fontconfig for cache symlinks (Chrome 154 bug)
[Path]
PathChanged=%h/.cache/fontconfig
Unit=fontconfig-symlink-guard.service
[Install]
WantedBy=default.target[Unit]
Description=Remove foreign symlinks from ~/.cache/fontconfig (Chrome 154 bug)
[Service]
Type=oneshot
ExecStart=%h/.local/bin/fontconfig-symlink-guardchmod +x ~/.local/bin/fontconfig-symlink-guard
systemctl --user daemon-reload
systemctl --user enable --now fontconfig-symlink-guard.pathPathChanged= on a directory fires when an entry is created in it, so there’s no polling. The rebuild itself writes into the same directory and triggers the unit again, which then finds no symlinks and exits.
To test it, I planted a symlink by hand. It was gone within 50 ms, the rebuild took three seconds, and the notification popped up. That leaves a three-second window in which a newly started Qt app could still read the bad cache. Apps that are already running have loaded the cache before it changed.
Open bug reports
My web search came up empty, but the Chromium issue tracker is poorly indexed. When I went to file the bug, two other people had already reported it the same day, both on fontconfig 2.15:
- Issue 565052663, “Chrome 154 on Linux hijacks ~/.cache/fontconfig”, has the same symlinks (38 font directories and 114 symlinks there), the same KaTeX font coming back from
fc-match, and Konsole and Discover segfaulting inside libfontconfig. It also notes that Chrome 153.0.8010.52 wrote plaincache-11files next to the system’scache-9and left those alone, and that making the cache directory read-only doesn’t help, because Chrome’s fontconfig changes the permissions back. Its workaround is to downgrade to 153 and hold the package, which works at the cost of a browser that no longer gets updates. - Issue 565132857, on Kubuntu 24.04.5. It reports 41
cache-12files and 123 symlinks, a black screen with a movable pointer, and the symlinks back at the next Chrome start after the cache had been moved aside.
Both were still open with status New when I wrote this. If you’re hit by this, star them: it’s how the Chrome team sees how many people are affected.
Two older reports come close without being the same:
- Problems with Qt? on the Arch Linux forums, from June, has the same backtrace on fontconfig 2.18.x, triggered by WOFF fonts. The workarounds there are downgrading to 2.17.1, removing the WOFF files, or running
fc-cache -r. - brave/brave-browser#57891, from August, is the same clash in reverse: a system fontconfig 2.18 writes
cache-12, and Brave’s older bundled fontconfig can’t read it, so CJK text turns into boxes. It also establishes thatcache-12is the fontconfig 2.18 format.
What still isn’t proven
I haven’t confirmed that Chrome 154 bundles fontconfig 2.18 specifically. cache-12 is the 2.18 format according to the Brave report, but Chromium’s commit history for its copy of fontconfig returned a 403 when I tried to read it.
I also don’t know why Chrome writes the symlinks at all. The first bug report found the string FONTCONFIG_NO_CHECK_CACHE_VERSION in the Chrome binary and says it may or may not be related.
The WOFF entries might not be the actual trigger. Every entry that lost its name was a WOFF file, and the Arch thread crashes on WOFF fonts, but I haven’t removed the KaTeX fonts to check.
Keeping the cache-12 files may not be enough to stop the symlinks from coming back. I have one Chrome start without them, and another reporter had a start where they returned after the whole cache was moved aside. The path unit covers both cases, so it stays until Chrome ships a fix.
As for who else is affected: all three of us are on fontconfig 2.15, one on Kubuntu 24.04.5. According to the first report, Ubuntu 26.04’s fontconfig 2.17.1 still writes format 9, so I’d expect newer Ubuntu releases to follow the symlinks the same way. The Brave report puts cache-12 at fontconfig 2.18. On a distribution that already ships 2.18 the symlinks point at the native format, so they shouldn’t matter there, and the Arch thread is the more likely problem.
TL;DR
- After Chrome 154 updated, Plasma went into a crash loop and every new Qt app segfaulted inside fontconfig.
- Chrome had written 37 format-12 cache files to
~/.cache/fontconfigand turned the format 9, 10 and 11 names into symlinks to them. - Ubuntu's fontconfig 2.15 followed those symlinks and misread the cache; deleting them and running
fc-cache -ffixed it. - A systemd path unit now keeps the symlinks away until Chrome ships a fix.