Vor einigen Monaten ließ sich die CIFS-Freigabe meiner Fritzbox nicht mehr ordentlich einbinden bzw. kam es zu merkwürdigen I/O-Fehlern bei einigen Lesezugriffen. Da dies mit einem Wechsel auf die Testversion von Fritz-OS zusammenfiel, hatte ich die Schuld innerlich bei Fritz gesehen. Aber die Tage habe ich mit einem alten Debian-Live-System auf die Freigabe zugegriffen und es funktionierte. Der Fehler scheint nicht im Fritz-OS, sondern im Linux zu liegen.

Für Linux hatte ich nun eine funktionsfähige und eine kaputte Version, was förmlich nach git-bisect schreit. Nur hatte ich keine Lust, das mit meinem Rechner zu probieren und 10 mal oder öfter immer wieder neustarten zu müssen. Aber mit Qemu hat sich eine super Lösung ergeben, die am Ende auch noch vollautomatisch durchlief und sich sehr leicht für ähnliche Fälle anpassen lässt.

Der Fairness halber muss ich sagen, dass ich einen Tag lang an der Lösung gebastelt habe, weil Gcc 10 nicht mit den älteren Kernelversionen funktioniert und mir alle Schritte am Beginn nicht so klar waren, wie sie jetzt hier erscheinen.

VM-Image erstellen

Der erste Schritt ist die Erstellung des Images mit debootstrap, was leider noch Root-Rechte erfordert. Für das Image kann man auch qemu-img verwenden, um zum Beispiel Snapshots nutzen zu können.

% truncate -s32G ~/kein_Backup/disk.img
% mke2fs -L Linux_Root -t ext4 ~/kein_Backup/disk.img
% sudo mount ~/kein_Backup/disk.img /mnt/other
% sudo debootstrap testing /mnt/other

Sollte sich im Nachhinein herausstellen, dass das Image zu klein ist, kann man es mit truncate und resize2fs vergrößern.

Zum Erleichtern der Arbeit habe ich für Root das Passwort entfernt und den Kernel1 installiert, um das System booten zu können.

  1. Es gibt auch noch die Variante linux-image-cloud-amd64, allerdings hat diese keine Unterstützung für 9P für den Datenaustausch. 

% sudo sed -i '1s/:x:/::/' /mnt/other/etc/passwd
% sudo chroot /mnt/other apt install --no-install-recommends linux-image-amd64
% sudo umount -d /mnt/other

Die folgenden Anpassungen können sowohl in der Chroot-Umgebung oder nach dem ersten Start ausgeführt werden. Die Meldung vor der Anmeldung habe ich angepasst, um bei den regelmäßigen Wechseln der Kernelversion den Überblick zu behalten, und das Paket console-setup ist notwendig, um die deutsche Tastatur einzurichten.

# echo LANG=C.UTF-8 > /etc/default/locale
# echo Europe/Berlin > /etc/default/locale
# echo debian-vm > /etc/hostname
# echo '[\d \t] Debian GNU/Linux \r bullseye/sid \n \l' > /etc/issue
# apt install console-setup

Weiterhin habe ich noch die Netzwerkverwaltung auf Systemd umgestellt, weil dies praktischer ist. Das Paket dbus ist dabei nur notwendig, wenn man networkctl zur Statusabfrage verwenden will.

# systemctl enable --now systemd-networkd
# apt install dbus
# networkctl
IDX LINK TYPE     OPERATIONAL SETUP
  1 lo   loopback carrier     unmanaged
  2 ens3 ether    routable    configured

Networkd im Hostsystem für DHCP einrichten

Mit Qemu verwende ich als Verbindung einen TAP/TUN-Netzwerkanschluss, damit ich Qemu (fast) ohne Root-Rechte starten kann. Hierbei ist sehr nützlich, dass Systemd-Networkd auch einen DHCP-Server integriert hat, den man für bestimmte Schnittstellen konfigurieren kann. Dabei lässt sich auch noch sehr einfach eine Paketweiterleitung und die Maskierung der Pakete einrichten, sodass das Gastsystem direkt ins Internet kann.

Diese Regeln sollen für alle TUN-Netzwerkanschlüsse gelten, die auf -vm enden, damit ich auch mehrere virtuelle Maschinen auf diese Weise betreiben kann. Abgespeichert habe ich die Konfiguration unter /etc/systemd/network/19-vm-tun.network, wobei man die Nummer im Dateinamen variieren muss, damit die Regeln auch wirklich auf den Anschluss zutreffen. Dies geht am besten mit networkctl reload und networkctl status server-vm.

[Match]
Driver=tun
Name=*-vm

[Network]
Address=0.0.0.0/24
Address=::/64
DHCPServer=yes
IPForward=yes
IPMasquerade=yes

[DHCPServer]
#EmitDNS=no
DNS=192.168.178.1

Start der virtuellen Maschine

Für den Start der virtuellen Maschine nutze ich das Zsh-Skript start-vm, um gewisse Konfigurationen für den Aufruf zu treffen, den Kernel und die Initrd aus dem Dateisystem zu extrahieren und Qemu selbst damit aufzurufen.

Wenn der Benutzer des Skripts Mitglied in der Gruppe kvm ist, benötigt man für den Aufruf von Qemu keine Root-Rechte. Allein für die Erstellung des Netzwerkanschlusses sind Root-Rechte notwendig, die ich einfach mit sudo und einem entsprechenden Eintrag mit sudoedit /etc/sudoers.d/qemu-tap-up erlange:

# used by qemu-tap-up
%kvm    ALL= (root) NOPASSWD: /usr/bin/ip tuntap add dev *-vm mode tap user *

Alternativ lässt sich auch der TAP-Netzwerkanschluss von Systemd-Networkd mit folgender Konfiguration verwalten. Da das Gerät aber nicht ständig bei mir im System präsent seien soll, habe ich die Variante mit sudo gewählt.

[NetDev]
Description=Virtual network for Qemu Debian-VM
Kind=tap
Name=debian-vm

[Tap]
Group=kvm

Als sehr praktisch hat sich erwiesen, dass Qemu in der Oberfläche die die serielle Schnittstelle integriert hat, die man über Strg+Alt+3 oder den Menüpunkt Ansicht, serial0 öffnen kann. Zurück zum Konsole kommt man wieder mit Strg+Alt+1 oder dem Menüpunkt Ansicht, virtio-vga. Für die serielle Schnittstelle funktioniert nämlich das Kopieren von Text mit der Maus, das Einfügen mit der mittleren Maustaste (bzw. linke und rechte Maustaste gleichzeitig oder linke Maustaste auf dem Touchpad mit drei Fingern) und die Historie über einen Reboot hinweg.

Durch Systemd wird auch automatisch eine Anmeldung für die serielle Schnittstelle gestartet, wenn es feststellt, dass diese vorhanden ist. Damit Linux mit der größer als üblichen seriellen Ausgabe zurechtkommt, muss man nach dem Login setterm --resize ausführen.

Datenaustausch mit dem Hostsystem

Plan 9 ist eigentlich ein anderes Betriebssystem aus den 1980er Jahren, das immer als Fortentwicklung der Unix-Prinzipien beschrieben wird, aber keine solch große Verbreitung wie Unix oder Linux gefunden hat. Dennoch gibt es im Kernel und in Qemu einen Treiber für das Dateisystem 9p, worüber Host- und Gastsystem sehr leicht Dateien austauschen können.

Für Qemu bedarf es nur der Option -virtfs local,mount_tag=share,path=…,security_model=none mit dem entsprechenden Verzeichnis im Hostsystem und im Gastsystem kann man mit dem Eintrag share /mnt 9p noauto,trans=virtio,version=9p2000.L 0 0 in /etc/fstab die Gegenseite anbinden.

Irgendwie kann man auch noch für die grafische Oberfläche die Zwischenablage beider Systeme verbinden, aber das ist mir noch gelungen. Hinweise werden gern entgegen genommen. 🙂

Automatische Fehlersuche

Für meine Fehlersuche im Kernel bedarf es natürlich der Kernelquellen und entsprechender Programme und Bibliotheken für den Bau. Außerdem ist das Programm ccache sehr nützlich, wenn man mit git-bisect durch die Versionen springt, um sich Kompilierarbeit zu ersparen1.

  1. Am Ende kam ccache -s auf eine Trefferquote von 10 %. 

Weiterhin war für meinen Fall das Paket cifs-utils notwendig, um die Freigabe einbinden zu können, und nach einigem Probieren hat sich herausgestellt, dass die Version 10 von Gcc alte Kernelversionen nicht bauen kann oder diese Versionen mit der entsprechenden Anpassung (per cherry-pick) mit der Meldung »stack-protector: Kernel stack is corrupted in: start_secondary« abstürzen. Deshalb habe ich noch die Version 9 installiert.

Der Wechsel des Kernels beim Neustart lässt sich sicher auch irgendwie mit dem Aufruf von Qemu lösen1, aber es gibt kexec, womit der Kernel selbst einen neuen Kernel laden kann. Der Anfang der Suche nach dem fehlerhaften Commit war recht leicht:

  1. Eine bestimmte Version lässt sich mit dem Aufruf version=-5.4.0 ./start-vm starten. 

# apt install --no-install-recommends build-essential bc bison flex lz4 libssl-dev libelf-dev \
    git ca-certificates ccache cifs-utils gcc-9 kexec-tools

# git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
# cd linux
# git bisect start
# git reset --hard v5.7
# ~/run compile && ~/run restart

# ~/run test; echo $?
32
# cd linux
# git bisect bad
# git reset --hard v4.19
# ~/run compile && ~/run restart

Zentral für die ganze Arbeit ist das folgende Skript run, bei dem die Teile auch einzeln aufgerufen werden können, um sie zum Beispiel am Anfang zu verwenden:

  • test führt die Befehle aus, um zu entscheiden, ob die laufende Kernelversion gut oder schlecht ist. Während der Arbeit hat sich gezeigt, dass bei einigen Kernelversionen der Aufruf von md5sum blockiert ist, weshalb der Einzeiler zuvor nach 4 Sekunden Pause den Prozess beendet.
  • config: beim Hin- und Herspringen zwischen den Versionen habe ich in der Vergangenheit erlebt, dass die Konfiguration kaputt geht. Deshalb soll sie immer wieder von der Debian-Konfiguration abgeleitet werden. Dabei muss aber die Einstellung für die Trusted-Keys gelöscht und die Erstellung der indivudellen Verion aktiviert werden, damit die Zwischenversionen unterscheidbar sind.
  • compile übersetzt und installiert den Kernel.
  • restart bootet mit dem neuen Kernel oder die Version, die als Paramter angegeben wurde.
#!/bin/sh

set -e

do_test() {
    mount -t cifs -o user=public,password=… //fritz.box/fritzi /mnt
    (sleep 4 && pid=$(pidof -sz md5sum) && kill $pid) &
    md5sum /mnt/WD_blau/Public/Jörg-Backup/Archiv/keys/63b0d55eb795822facba850363d5148ce657db559552ef3a5cdf6623d110bb98
}

config() {
    cp /boot/config-5.7.0-2-amd64 .config

    sed -i '/CONFIG_SYSTEM_TRUSTED_KEYS=/s/=.*/=""/;
      /CONFIG_LOCALVERSION_AUTO/cCONFIG_LOCALVERSION_AUTO=y' .config

    make oldconfig </dev/null
}

compile() {
    make -j$(nproc) "$@" all
    make modules_install install
}

restart() {
    ver=${1:-$(make -s kernelrelease)}
    kexec -l /boot/vmlinuz-$ver --initrd=/boot/initrd.img-$ver --reuse-cmdline
    systemctl kexec
}

cd ~/linux
PATH=/usr/lib/ccache:"$PATH"
export CCACHE_CC=/usr/bin/gcc-9

case "$1" in
  test)
    do_test
    ;;

  compile|restart|config)
    "$@"
    ;;

  all)
    if [ "$(make -s kernelrelease)" != $(uname -r) ]
    then
        echo "Kernel version doesn't match:" \
          "$(make -s kernelrelease) != $(uname -r)" >&2
        exit 1
    fi

    rev=$(git rev-parse HEAD)
    if test "$rev" == "$(git rev-parse refs/bisect/bad)"
    then
        echo
        echo "We are done"
        exit
    fi

    if do_test
    then
        echo git bisect good
        git bisect good
    else
        echo git bisect bad
        git bisect bad
    fi

    if test "$rev" == "$(git rev-parse HEAD)"
    then
        exit
    fi

    config
    compile
    restart
    ;;

  *)
    echo 'usage: run test|config|compile|restart|all' >&2
    exit 1
esac

Nach jedem Start müssen die Einzelschritte ausgeführt und entsprechend auf die Ergebnisse reagiert werden. Der komplette Ablauf vom Test über den Bau bis zum Neustart lässt sich aber vollständig automatisieren, sodass nach dem Login der Aufruf run all genügt.

Automatische Anmeldung

Der letzte händische Schritt des Aufrufs von run lässt sich auch noch mit der automatischen Anmeldung lösen. Mit systemctl edit getty@tty1.service kann man festlegen, dass der Benutzer root sofort auf der ersten Konsole angemeldet wird.

[Service]
ExecStart=
ExecStart=/usr/sbin/agetty -a root --noclear %I $TERM
Restart=on-success

Mit dem folgenden Eintrag in ~/.profile führt die Bash dann auch run aus und die Suche läuft vollautomatisch durch, wenn es nicht zu unerwarteten Fehlern kommt.

case "$(tty)" in
  /dev/tty1)
    ~/run
    ;;
  /dev/ttyS0)
    setterm --resize
    ;;
esac

Als eine kleine Erweiterung wäre noch möglich, die automatische Anmeldung für die serielle Schnittstelle einzurichten und beim Aufruf von Qemu die Ausgaben über diese Schnittstelle in eine Datei schreiben zu lassen. Somit bekäme man ein Protokoll des Ablaufs. Die einzelnen Schritte bei git-bisect lassen sich aber jederzeit mit git bisect log nachvollziehen.

Weitere Informationsquellen