Dmytro Oliinyk · 4. August 2026 · 10 Min. Lesezeit
Vorfall · Linux
15 Stunden Downtime: schuld war die Grafikkarte
Ein Reboot über SSH nimmt den OVH-VPS für 15 Stunden vom Netz, weil PID 1 auf drm_modeset_lock wartet. Der Schuldige ist qxl, eine emulierte Grafikkarte, die auf einem Server ohne Monitor gar nicht existieren müsste. Diagnose, sysrq-Rettung und die Zeile in GRUB, die das nächste Mal verhindert.
Am 3. August habe ich über SSH reboot getippt. Die Maschine ist nie zurückgekommen. Alle Container weg, alle Dienste weg, kein SSH mehr. Sie beantwortete Pings und sonst nichts.
Der Reboot-Knopf im OVH-Panel änderte daran nichts. Am 4. August um acht Uhr morgens hat der Support die Maschine von Hand neu gestartet. Fünfzehn Stunden Downtime für einen Befehl mit sechs Buchstaben.
Damit fängt die eigentliche Geschichte aber erst an, denn zurück war der Server nur äusserlich. Das hier ist der Weg von dem Moment an, an dem die Pakete nicht mehr sauber installierten, bis zu der Zeile in /etc/default/grub, die das nächste Mal verhindert. Die Auflösung nehme ich vorweg, weil ich sie selbst zu spät hatte: Der Hypervisor war es nicht. Es ist ein bekannter Fehler im Grafiktreiber des Gasts, den der Kernel seit Jahren kennt und heute bewusst in Kauf nimmt.
Die Maschine#
Ein VPS bei OVH in Zürich, Debian, alle Dienste in Docker-Containern, kein Kubernetes. Nichts davon ist exotisch, und genau darum trifft der Fehler eine ganze Klasse von Servern.
| Bestandteil | Wert | Anmerkung |
|---|---|---|
| Hypervisor | QEMU/KVM bei OVH | Standard-VPS, keine Sonderkonfiguration |
| Distribution | Debian 13 (Trixie) | Stable, von mir installiert |
| Kernel | 6.12.100+deb13-amd64 | die Nummer wird im Abschnitt zur Schuldfrage wichtig |
| Grafik | qxl 0.1.0 auf 0000:00:01.0 | emuliert, es hängt kein Monitor daran |
| Kernel-Parameter | hugepages=1024 | 2 GB, die ich für Postgres reserviert habe |
| Dienste | Docker mit restart: unless-stopped | laufen unabhängig von systemd |
Die Kernel-Version ist hier keine Nebensache, sondern der Kern der Geschichte. Der Commit hinter dem Fehler weiter unten wurde zurückgenommen, wieder eingebaut und in einzelnen Distributionen erneut umgedreht. Welche Variante auf deiner Maschine liegt, entscheidet uname -r.
Was zuerst auffiel#
Der Server lief wieder, die Container waren über restart: unless-stopped von selbst zurückgekommen, und SSH funktionierte. Ich wollte Pakete aktualisieren und merkte, dass etwas nicht stimmte. systemctl daemon-reload brachte:
Reload daemon failed: Transport endpoint is not connected
systemctl is-system-running hing einfach. Kein Timeout, keine Fehlermeldung. Das war stabil und nicht einmalig. Also PID 1 prüfen:
ps -p 1 -o pid,comm,stat
PID COMMAND STAT
1 systemd Ds
Ds bedeutet ununterbrechbarer Schlaf. Ein Prozess in diesem Zustand verarbeitet keine Signale, auch kein SIGKILL. Er steckt in einem Kernel-Syscall fest und kommt erst wieder heraus, wenn die erwartete I/O-Operation endet, was in diesem Fall nie passiert.
Die offenen Dateideskriptoren waren unauffällig:
ls /proc/1/fd | wc -l
# 127
cat /proc/1/limits | grep -i "open files"
# Max open files 1073741816 1073741816 files
Von Resource Exhaustion keine Spur. Dann dmesg:
dmesg -T | tail -40
Alle 15 Sekunden dieselbe Zeile, seit Minuten:
[TTM] Buffer eviction failed
TTM ist die Speicherverwaltung unter DRM. Das ist die Grafik-Pipeline, auf einem Server ohne Bildschirm.
Der Stack#
cat /proc/1/wchan; echo
cat /proc/1/syscall
cat /proc/1/stack 2>/dev/null | head -20
wchan war drm_modeset_lock. Der Syscall war ein write. Der Stack, vom Aufrufer zum innersten Frame:
write() → tty → n_tty_write → con_write → do_con_write →
fbcon_switch → bit_update_start → fb_pan_display →
drm_fb_helper_pan_display → drm_client_modeset_commit_locked →
drm_client_modeset_commit_atomic → drm_atomic_get_plane_state →
drm_modeset_lock
systemd hat versucht, etwas auf die Konsole zu schreiben. Der normale write() auf /dev/console ging durch die Framebuffer-Konsole, die Framebuffer-Konsole hat DRM angesprochen, und dort stand der Mutex, der nicht mehr freigegeben wurde. Dasselbe Subsystem, das alle 15 Sekunden Buffer eviction failed meldet.
Damit ist der Fall klar: Der Grafiktreiber hängt, und mit ihm der gesamte Konsolen-Pfad. Jeder Prozess, der auf die Konsole schreibt, landet ebenfalls im D-Zustand, und PID 1 war nur der erste davon.
Raus ohne systemd#
systemctl reboot und shutdown laufen über denselben systemd, der hier gerade nicht antwortet. Sie würden in denselben Deadlock laufen wie beim Reboot am Tag zuvor, und diesmal wusste ich, was das kostet: fünfzehn Stunden und ein Support-Ticket. Ein harter Reset aus dem Panel kam auch nicht in Frage, solange Daten im Schreibcache lagen.
Docker läuft unabhängig von systemd. Die Container liessen sich sauber stoppen:
docker ps
docker stop $(docker ps -q)
sync; sync
Dann die Magic-SysRq-Sequenz, die der Kernel selbst ausführt, ohne systemd. Die Reihenfolge ist in der Kernel-Dokumentation beschrieben:
echo 1 > /proc/sys/kernel/sysrq
echo s > /proc/sysrq-trigger # sync
sleep 5
echo u > /proc/sysrq-trigger # remount read-only
sleep 5
echo b > /proc/sysrq-trigger # reboot
Die 1 schaltet alle sysrq-Funktionen frei, nicht nur die drei hier benutzten. Wer das nicht dauerhaft offen haben will, setzt den Wert nach dem Neustart wieder auf den vorherigen zurück.
Bitte in dieser Reihenfolge
Erst s, dann u, dann b. Wer direkt auf b geht oder im Panel den harten
Reset drückt, verliert alles, was noch im Schreibcache steht.
SSH bricht bei b sofort ab. So soll es sein, die Dateisysteme sind vorher synchronisiert und read-only. Das ist deutlich sicherer als der harte Reset aus dem Panel.
Das Panel von OVH vorher öffnen, aber ohne grosse Erwartungen. Bei mir hat der Reboot-Knopf dort nichts bewirkt, gebraucht hat es am Ende einen Menschen beim Anbieter. Wer nach zehn Minuten keine Reaktion sieht, macht besser gleich ein Ticket auf, statt weiter auf denselben Knopf zu drücken.
Nach dem Reboot#
uptime
systemctl is-system-running
# running
systemctl --failed
# 0 loaded units listed.
docker ps
systemd war wieder ansprechbar, und die Container liefen. Einer fehlte und musste von Hand gestartet werden. In dmesg stand jetzt:
[drm] Initialized qxl 0.1.0 for 0000:00:01.0 on minor 0
fbcon: qxldrmfb (fb0) is primary device
qxl 0000:00:01.0: [drm] fb0: qxldrmfb frame buffer device
Der Treiber, der da geladen wird, heisst qxl. Das ist die von QEMU emulierte Grafikkarte für SPICE. Auf diesem Server hängt kein Bildschirm daran, und niemand hat je eine SPICE-Sitzung dorthin geöffnet.
Der Fix#
Auf einem Headless-VPS braucht man weder qxl noch den restlichen DRM-Stack. Es gibt zwei Wege, und einen dritten, der die meisten Leser schneller ans Ziel bringt.
Der einfache Weg#
Wenn dein Anbieter das Grafikmodell im Panel umstellen lässt, stell es von qxl auf virtio-gpu um und hör hier auf zu lesen. Das ist auch die Empfehlung, die in den Bug-Reports weiter unten immer wieder auftaucht. Bei OVH hatte ich diese Option nicht, also blieb der Weg über den Gast.
Serielle Konsole#
Bevor ich am Grafikstack schraube, will ich einen zweiten Weg auf die Maschine haben. Der bestehende Eintrag in /etc/default/grub war:
GRUB_CMDLINE_LINUX_DEFAULT="hugepages=1024 "
hugepages=1024 reserviert 2 GB für Huge Pages. Die habe ich von Hand für Postgres eingetragen, und sie bleiben stehen. Dazu kommt die serielle Konsole:
cp /etc/default/grub /etc/default/grub.bak
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="hugepages=1024 "/GRUB_CMDLINE_LINUX_DEFAULT="hugepages=1024 console=tty0 console=ttyS0,115200"/' /etc/default/grub
console=tty0 console=ttyS0,115200 gibt die Kernel-Ausgabe auf beide Konsolen aus. Die zuletzt genannte bekommt /dev/console, also landen die Meldungen im Ernstfall auf der seriellen Leitung.
Das Modul aussperren#
Für den Treiber selbst gibt es zwei Varianten. nomodeset als Kernel-Parameter verhindert, dass der Kernel überhaupt Modesetting übernimmt, und trifft damit alle DRM-Treiber. Gezielter ist es, nur das eine Modul auszusperren:
echo "blacklist qxl" > /etc/modprobe.d/blacklist-qxl.conf
echo "# qxl DRM stack deadlocked systemd on 2026-08-03, see drm_modeset_lock" >> /etc/modprobe.d/blacklist-qxl.conf
update-initramfs -u
update-grub
blacklist verhindert das automatische Laden über die PCI-Kennung, nicht aber ein Laden als Abhängigkeit oder ein explizites modprobe qxl. Wenn du ganz sicher gehen willst, nimm stattdessen install qxl /bin/true, das schlägt jeden Ladeversuch tot. Für diesen Fall reicht die Blacklist, weil qxl hier nur über den PCI-Bus kommt.
Vor dem Reboot prüfen:
grep -c "console=ttyS0" /boot/grub/grub.cfg
# > 0
Dann ein normaler reboot. Diesmal funktioniert er, weil systemd wieder gesund ist.
Nach dem Start:
lsmod | grep qxl
# (leer)
dmesg -T | grep -ci drm
# 2
Statt Hunderten von Zeilen bleiben zwei übrig. Die grafische KVM-Konsole im OVH-Panel zeigt weiterhin Text, jetzt über vgacon statt über den Framebuffer. Der serielle Pfad ist als Reserve da.
Die Schuldfrage#
Ich hatte zuerst den Hypervisor im Verdacht. Die Argumentation klang gut: Ein Gast kann sich sein eigenes emuliertes Gerät nicht selbst kaputt machen, also muss der Host es kaputt gemacht haben. Sie ist falsch, und das lässt sich nachlesen.
Der Treiber im Gast#
Der Fehler ist alt und dokumentiert. In Debian-Bug #1054514 hat ihn jemand 2023 mit einem Reproduzierer und einer Bisect-Sitzung auf den Commit 5a838e5d5825 eingegrenzt, "drm/qxl: simplify qxl_fence_wait", drin seit 5.13. Die Symptome dort sind exakt meine: [TTM] Buffer eviction failed im Sekundentakt und Prozesse, die im Kernel hängen bleiben.
Interessant wird es danach. Der Commit wurde mit 07ed11afb68d zurückgenommen, und dieser Revert brachte einen anderen Deadlock zwischen dem Konsolen-Lock und dem Worker-Pool, an dem Testmaschinen gar nicht mehr durchbooteten. Also hat Linus den Revert mit 3628e0383dd3 in 6.9 wieder rückgängig gemacht, mit einer bemerkenswert offenen Begründung im Commit: Das könne die Buffer eviction failed-Meldungen zurückbringen, und das sei einem System vorzuziehen, das nicht bootet. Nachzulesen im Debian Security Tracker zu CVE-2024-36944, inklusive der Versionen, in denen die einzelnen Zweige stehen. In dieser Tabelle steht für Trixie-Security die Version 6.12.100-1, und genau die läuft auf dieser Maschine. Ich habe also kein seltenes Pech gehabt, ich habe den dokumentierten Zustand bekommen.
Dass der Zustand damit nicht erledigt ist, zeigt Debian-Bug #1139265 auf einem aktuellen Kernel: jemand anderes beschreibt dort dieselbe Kette über fbcon_switch und fb_pan_display bis drm_modeset_lock, dieselben Prozesse im D-Zustand und dieselbe Meldung alle 15 Sekunden.
OVHs Anteil#
Nicht den Deadlock, aber das Gerät. Einem Headless-VPS eine qxl-Karte zu geben, ist eine fragwürdige Voreinstellung. qxl kommt aus der Desktop-Virtualisierung mit SPICE. Auf einem Server ohne Monitor reicht virtio-gpu, einfaches VGA oder gar nichts, und die Konsole läuft über Serial. Man zahlt Stabilität für ein Gerät, das man nie benutzt, und man kann es im Panel nicht abwählen. Dazu kommt, dass der Reboot-Knopf in genau diesem Panel eine hängende Maschine nicht neu startet. Den Hang hat der Gast verursacht, verlängert hat ihn diese Schaltfläche, bis am Morgen jemand beim Anbieter von Hand eingriff.
Die Freisprüche#
Debian lädt DRM-Treiber für alles, was auf dem PCI-Bus erscheint. Das ist ein vernünftiger Default für Desktop-Maschinen und keine Schuld des Distributors. Und hugepages=1024 stammt von mir, das habe ich für Postgres so gewollt, aber mit diesem Stack hat es nichts zu tun. Der hängt ausschliesslich an Grafik und TTY.
| Beteiligter | Anteil | Warum |
|---|---|---|
qxl-Treiber im Gast | Primärursache | Bekannter Fehler seit 5.13, upstream bewusst in Kauf genommen |
| Konsolen-Pfad im Kernel | Verstärker | write() über fbcon und DRM hat weder Timeout noch Fehlerpfad |
| OVH | Voreinstellung | qxl auf einem Server ohne Monitor, nicht abwählbar |
| Debian | keiner | Lädt Treiber für vorhandene PCI-Geräte, wie erwartet |
| Meine Konfiguration | keiner | Die 2 GB Huge Pages sind meine Entscheidung, sie berühren weder DRM noch TTY |
Der Verstärker in Zeile zwei ist der Teil, der mich am meisten stört. Ein Treiber, der sein Gerät als tot erkennt, sollte den Aufrufer mit einem Fehler zurückschicken. Hier bleibt stattdessen jeder Schreibversuch auf die Konsole für immer hängen, und der erste, den es erwischt, ist PID 1.
Was bleibt#
Der Fix sitzt in GRUB und in einer Datei unter /etc/modprobe.d. Genau dafür gibt es Kernel-Parameter und modprobe-Konfiguration: um Hardware, die man nicht braucht und die kaputt gehen kann, aus dem Spiel zu nehmen, bevor sie PID 1 mitnimmt.
Die eigentliche Lehre ist unbequemer. Ich hatte einen Verdächtigen, eine plausible Erklärung und keinen Beweis, und ich war nahe dran, das so zu veröffentlichen. Zwischen "der Host muss es gewesen sein" und einem Bisect-Ergebnis mit Commit-ID liegen zehn Minuten Suche.
Wenn du denselben Stack siehst, schreib mir. Vor allem, wenn dein Kernel neuer ist als meiner und die Kette trotzdem hängt, das würde ich gern wissen, bevor es mir ein zweites Mal passiert.