Dmytro Oliinyk

Dmytro Oliinyk · 4. August 2026 · 13 Min. Lesezeit

Auf Mastodon teilen

Vorfall · Linux

15 Stunden Downtime: schuld war die Grafikkarte

Fünfzehn Stunden Downtime, verursacht von einer Grafikkarte, die es physisch gar nicht gibt. Ein reboot über SSH setzt PID 1 hinter einem emulierten Gerät fest, auf einem Server, an dem nie ein Monitor hing. Die Diagnose, die Rettung ohne harten Reset und die eine Zeile in GRUB, die eine Wiederholung verhindert.

OVH hat mir eine Mail geschickt. Mein VPS bekommt eine schnellere Leitung, von 400 Mbit auf 1 Gbit, und dafür muss ich ihn nur neu starten. Schön.

Ich habe vorher gemessen, weil ich den Unterschied hinterher sehen wollte. 41 MB/s. Das sind 400 Mbit, die alte Zahl stimmte also immerhin.

Also habe ich gut gelaunt sudo reboot getippt und mich auf die üblichen fünfzehn Sekunden eingestellt.

Und SSH kam nicht zurück. Ich habe ihm zwei, drei Minuten gegeben, bevor ich mir eingestanden habe, dass etwas nicht stimmt.

Aber im OVH-Panel stand Active. Für die war die Maschine in Ordnung. Also habe ich dort auf Reboot gedrückt, das Naheliegende, und genau da ist sie richtig gestorben. Fünfzehn Stunden.

Ich habe die KVM-Konsole und SSH probiert und bei beiden nichts bekommen. Aber Pings kamen zurück, und das war die ganze Zeit das einzige Lebenszeichen. Im Nachhinein war das der Kernel, der mir sagte, dass er noch da ist und alles über ihm nicht. Ich habe ein Ticket aufgemacht und dann einfach gelesen, woran so etwas liegt, weil der OVH-Support keinen guten Ruf hat und ich mit einer Woche gerechnet habe. Am Abend habe ich angefangen, mir andere Anbieter anzusehen.

Viel gemacht habe ich in dieser Nacht nicht. Ich habe gelesen. Passiert das Gästen überhaupt, kann es am Hypervisor liegen, was hätte ich auf einem frisch installierten Debian falsch konfiguriert haben sollen. Die letzte Frage kam immer wieder, und eine gute Antwort hatte ich nie, und ungefähr so habe ich mir eingeredet, dass der Host schuld ist.

Und um acht Uhr am nächsten Morgen war er wieder da, von Hand neu gestartet auf deren Seite. Ich habe zuerst die Leitung geprüft. 1 Gbit. Gut.

Also, die kurze Fassung, weil ich selbst viel zu lange gebraucht habe. Der Hypervisor war es nicht. Es ist ein bekannter Fehler im Grafiktreiber des Gasts, und der Kernel kennt ihn seit Jahren und liefert ihn trotzdem aus, mit Absicht.

Die Maschine ist ein VPS bei OVH in Zürich, Debian, alle Dienste in Docker-Containern, kein Kubernetes. Nichts davon ist exotisch. Darum trifft der Fehler eine ganze Klasse von Servern. Die genauen Zahlen kommen unten, wenn es um die Schuld geht.

Falls du gerade eine hängende Maschine vor dir hast und nicht meine Geschichte suchst, der Weg da raus ohne harten Reset steht unter Raus ohne systemd und der dauerhafte Fix unter Was ich geändert habe. Der Rest kann warten, bis die Kiste wieder läuft.

Der Fehler, den ich nicht gesucht habe#

Dann fiel mir ein, dass OVH eine 2027er-Reihe mit etwas schwächeren Werten hat, langsamerer Speicher, schwächere CPU, und ich wurde nervös, dass sie mich stillschweigend dorthin geschoben hatten. Also habe ich Benchmarks laufen lassen. Die Benchmarks waren in Ordnung.

Aber während ich iperf3 installierte, kam das hier in Rot.

Reload daemon failed: Transport endpoint is not connected

Ich hatte keine Ahnung, was das damit zu tun haben sollte. Es ist ein ganz normales Paket. systemctl is-system-running brachte dasselbe, und es hing, statt einen Fehler zu liefern. Kein Timeout, keine Meldung. Ich habe es gegoogelt. systemd tot.

Und da fiel bei mir der Groschen. Noch ein ganz normaler Reboot, und die Maschine geht direkt wieder in diesen Zustand, und der Hypervisor bekommt sie da nicht heraus. Also PID 1 prüfen:

ps -p 1 -o pid,comm,stat
   PID COMMAND STAT
      1 systemd Ds

Ds. Ununterbrechbarer Schlaf. Der Prozess steckt in einem Kernel-Aufruf und nichts holt ihn da raus, auch kein SIGKILL, weil in diesem Zustand gar keine Signale zugestellt werden. Theoretisch wusste ich das. Auf PID 1 ist es etwas anderes.

Von aussen sah übrigens alles gut aus. Container liefen, Dienste antworteten, jede Uptime-Prüfung grün. Kaputt war der Teil, den von aussen niemand abfragt.

Also bin ich zuerst der langweiligen Erklärung nachgegangen, der, die ich mir gewünscht hätte. Ein Deskriptor-Leck, irgendein Limit.

ls /proc/1/fd | wc -l
# 127
cat /proc/1/limits | grep -i "open files"
# Max open files            1073741816           1073741816           files

127 offen, Limit in den Millionen. Auch nicht. Also dmesg:

dmesg -T | tail -40

Etwa zwanzig Mal dieselbe Zeile.

[TTM] Buffer eviction failed

TTM ist die Speicherverwaltung unter DRM. Das ist die Grafik-Pipeline, auf einem Server ohne Bildschirm.

Wo PID 1 festhing#

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()                            systemd schreibt auf /dev/console
  n_tty_write, con_write           TTY
  do_con_write, fbcon_switch       Framebuffer-Konsole
  bit_update_start, fb_pan_display
  drm_fb_helper_pan_display        DRM
  drm_client_modeset_commit_locked
  drm_client_modeset_commit_atomic
  drm_atomic_get_plane_state
  drm_modeset_lock                 hier bleibt alles stehen

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 dmesg mit Buffer eviction failed vollschreibt.

Damit war die Sache klar. Der Grafiktreiber hing, und mit ihm der ganze Konsolen-Pfad. Jeder Prozess, der auf die Konsole schreibt, landet ebenfalls im D-Zustand, und PID 1 war nur der erste davon. Wen es sonst noch erwischt hat, zeigt ein Filter über die Prozessliste:

ps -eo pid,stat,comm | awk '$2 ~ /^D/'

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

Eine Warnung vorweg. Das hier ist ein Notfallweg. Wenn systemctl reboot bei dir noch antwortet, nimm den. sysrq spricht direkt mit dem Kernel und übergeht jeden Shutdown-Hook, jede Unit und jedes Skript, das sonst beim Herunterfahren laufen würde. Darum funktioniert es hier. Und darum will man es sonst nirgends.

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.

Auf b zu drücken war unheimlich. Die Backups lagen da, und innerlich war ich sowieso schon halb beim nächsten Anbieter, also habe ich es einfach drauf ankommen lassen.

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 OVH-Panel vorher öffnen, aber ohne grosse Hoffnung. Ich habe dort ungefähr zwanzig Mal auf Reboot gedrückt. Dann bin ich in ihre API-Konsole gegangen und habe nach dem Task gesucht, und ich konnte ihn sehen, eingeplant, Status pending, und nirgends etwas, um ihn abzubrechen. Die beiden Endpunkte, die ich eigentlich wollte, standen direkt daneben, /vps/{serviceName}/stop und /vps/{serviceName}/start, und beide verweigerten sich, weil schon ein anderer Task lief. Das ist der Teil, der mich wirklich gestört hat. Ich konnte meine eigene Maschine nicht hart stoppen und wieder starten, weil die Anfrage, sie neu zu starten, festhing und nichts sie aufräumte. Am Ende brauchte es einen Menschen beim Anbieter. Wenn sich nach zehn Minuten nichts rührt, mach ein Ticket auf, statt den Knopf noch einmal 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

Dieser Treiber heisst qxl. Das ist die von QEMU emulierte Grafikkarte für SPICE, und eine SPICE-Sitzung hat auf diese Maschine noch nie jemand geöffnet.

Ob dieselbe Karte auch in deiner VM steckt, beantworten zwei Zeilen:

lspci | grep -i vga
lsmod | grep qxl

Was ich geändert habe#

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. Text bekomme ich auf der KVM-Konsole im Panel weiterhin, jetzt über vgacon statt über den Framebuffer. Der serielle Pfad ist als Reserve da.

Die Eckdaten#

Bevor es um Schuld geht, die genauen Zahlen:

BestandteilWertAnmerkung
HypervisorQEMU/KVM bei OVHStandard-VPS, keine Sonderkonfiguration
DistributionDebian 13 (Trixie)Stable, von mir installiert
Kernel6.12.100+deb13-amd64die Nummer wird gleich wichtig
Grafikqxl 0.1.0 auf 0000:00:01.0emuliert, es hängt kein Monitor daran
Kernel-Parameterhugepages=10242 GB, die ich für Postgres reserviert habe
DiensteDocker mit restart: unless-stoppedlaufen unabhängig von systemd

Diese Kernel-Version ist keine Nebensache, sie ist die ganze Geschichte. Der Commit hinter dem Fehler wurde zurückgenommen, wieder eingebaut und in einzelnen Distributionen erneut umgedreht. Welche Variante auf deiner Maschine liegt, entscheidet uname -r.

Die Schuldfrage#

Ich hatte zuerst den Hypervisor im Verdacht, und ich war mir ziemlich sicher dabei. 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: Das könne die Buffer eviction failed-Meldungen zurückbringen, und das sei besser als ein System, das nicht bootet. Dagegen lässt sich schwer argumentieren.

Der Debian Security Tracker zu CVE-2024-36944 führt die Versionen, in denen die einzelnen Zweige stehen. Dort steht für Trixie-Security die Version 6.12.100-1. 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, gemeldet auf 7.0.10+deb14-amd64 und damit auf einem deutlich neueren Kernel als meinem. 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 im Sekundentakt.

Überrascht hat mich, dass das immer noch aktuell ist. Kein Fehler von 2023, der behoben wurde, sondern einer, den man sich oben angesehen, abgewogen und behalten hat. Was auch heisst, dass ich damit vermutlich nicht allein sitze.

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 Hardware, die man nie benutzt, und das Panel bietet keine Möglichkeit, sie zu tauschen. Dazu kommt der Reboot-Knopf in genau diesem Panel, der eine hängende Maschine nicht neu startet, und eine API, die einen die Kiste nicht selbst stoppen lässt, solange ein festhängender Task in der Warteschlange die Tür blockiert. Den Hang hat der Gast verursacht. Auf fünfzehn Stunden gestreckt hat ihn diese Warteschlange.

In dieser Nacht war ich sicher, dass OVH einfach ein instabiler Anbieter ist. Sind sie nicht, und der Hänger ging auch nie auf ihr Konto. Was sie haben, ist ein unfertiges Panel und zu wenig Kontrolle über den eigenen VPS. Mit einem Stop und einem Start, die tatsächlich funktionieren, hätte ich vermutlich nie ein Ticket aufgemacht, und das Ganze wären zwanzig Minuten gewesen statt fünfzehn Stunden.

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.

BeteiligterAnteilWarum
qxl-Treiber im GastPrimärursacheBekannter Fehler seit 5.13, upstream bewusst in Kauf genommen
Konsolen-Pfad im KernelVerstärkerwrite() über fbcon und DRM hat weder Timeout noch Fehlerpfad
OVHVoreinstellungqxl auf einem Headless-VPS, nicht abwählbar
DebiankeinerLädt Treiber für vorhandene PCI-Geräte, wie erwartet
Meine KonfigurationkeinerDie 2 GB Huge Pages sind meine Entscheidung, sie berühren weder DRM noch TTY

Zeile zwei ärgert mich wirklich. Ein Treiber, der sein Gerät als tot erkennt, sollte den Aufrufer mit einem Fehler zurückschicken und den Lock loslassen. Dieser behält ihn. Jeder Schreibversuch auf die Konsole dahinter hängt dann für immer, und der erste in dieser Schlange ist PID 1.

Was bleibt#

Der Fix sitzt also in GRUB und in einer Datei unter /etc/modprobe.d. Genau dafür gibt es Kernel-Parameter und modprobe-Konfiguration. Man nimmt die Hardware, die man nicht braucht und die kaputt gehen kann, aus dem Spiel, bevor sie PID 1 mitnimmt.

Die Lehre, die ich mitnehme, 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.

Mastodon