Kernel panic sur un NAS Debian : récupérer une libc tronquée sans rebooter sur du vide

Kernel panic sur un NAS Debian : récupérer une libc tronquée sans rebooter sur du vide

·13 min de lecture·Mis à jour le 24 mai 2026
Pour qui

Vous administrez un Debian, Ubuntu, Proxmox ou tout autre système APT-based en production perso (NAS, homelab, VPS). Vous voulez comprendre pourquoi un simple unattended-upgrade peut briquer une machine - et comment la dépanner sans réinstaller. Niveau requis : à l'aise avec le shell, chroot, dpkg, debugfs.

Le symptôme

NAS injoignable en ping et en SSH. Sur la console physique, GRUB affiche Boot Option Restored puis un grub> solitaire. Un exit ramène le menu normal, mais avec une seule entrée kernel : pas d'ancien noyau, pas de mode recovery. Au boot :

Kernel panic - not syncing: Attempted to kill init! exitcode=0x00007f00

panic → do_exit.cold → do_group_exit → __x64_sys_exit_group
     → do_syscall_64 → handle_mm_fault → do_user_addr_fault

Segfault dans le PID 1. Systemd meurt au tout premier exec userspace, le kernel n'a pas le choix. Identique en retirant à la main les options de hardening via grub edit : le problème n'est pas dans la ligne kernel, il est juste après.

Le contexte matériel : un Terramaster F6-424 dont le TOS d'origine a été remplacé par une Debian 13 (Trixie) installée sur la clé USB interne de 4 Go. Les 6 baies hébergent d'anciens disques Synology avec des structures RAID5 + LVM + Btrfs montées sur /mnt/data. Le système est durci selon une baseline CIS / ANSSI : AppArmor en enforce, auditd actif, options kernel oops=panic slab_nomerge init_on_alloc=1 …, et GRUB_DISABLE_RECOVERY="true".

Presque chacun de ces choix va se retourner contre l'opérateur dans les heures qui suivent.

Diagnostic : boot live USB et debugfs

Après boot sur une Debian 13.5 XFCE live, lsblk -f confirme que le système est sur /dev/sdf (clé USB 3,75 Gio) : sdf1 en FAT32 pour /boot/efi, sdf2 en ext4 pour /.

Premier réflexe : fsck -n -f /dev/sdf2. Structure ext4 propre, aucune erreur de filesystem. La corruption n'est donc pas structurelle mais dans le contenu des fichiers.

debugfs permet de lire un FS ext4 sans le monter, donc strictement en read-only :

debugfs -R 'cat /etc/debian_version' /dev/sdf2
debugfs -R 'ls -l /boot' /dev/sdf2
debugfs -R 'stat /lib/systemd/systemd' /dev/sdf2
debugfs -R 'cat /var/log/apt/history.log' /dev/sdf2

Surprise dans /var/log/apt/ : history.log et term.log font 0 octet. Tronqués. Le .gz archivé contient l'historique antérieur.

Montage de la partition pour chrooter :

mount /dev/sdf2 /mnt/sysroot
mount /dev/sdf1 /mnt/sysroot/boot/efi
chroot /mnt/sysroot /lib/systemd/systemd
error while loading shared libraries:
/lib/x86_64-linux-gnu/libc.so.6: invalid ELF header

Et le verdict de file :

$ file /mnt/sysroot/usr/lib/x86_64-linux-gnu/libc.so.6
ISO-8859 text, with very long lines

La libc n'est plus un ELF. Elle fait pourtant 1 999 312 octets, soit quasiment la bonne taille, mais ses blocs sont du texte aléatoire. Le kernel charge, exec init (= systemd), le loader essaie de mapper libc, segfault, init meurt, panic.

La timeline reconstituée

history.log.1.gz raconte tout :

  • 21 février, 12:45 : première saturation pendant un apt purge. Erreur : mandb: impossible d'écrire dans /var/cache/man/... : Aucun espace disponible sur le périphérique.
  • 27 février, 06:45 : unattended-upgrade upgrade libnss3, déclenche les triggers libc-bin.
  • 28 février, 23:45 : history.log et term.log tronqués à zéro.

La deuxième saturation s'est produite pendant le trigger libc-bin. dpkg a écrit partiellement libc.so.6 : le fichier garde sa taille apparente, mais des blocs ne sont pas écrits ou sont écrasés par autre chose. ELF invalide.

Pourquoi le crash était inéluctable

Six décisions raisonnables prises isolément, catastrophiques combinées :

  1. Disque système de 3,5 Go pour un OS qui télécharge plusieurs centaines de Mo d'updates par mois.
  2. unattended-upgrade sans surveillance, sans alerte mail, sans check d'espace pré-transaction.
  3. GRUB_DISABLE_RECOVERY="true" : impossible de booter en single-user pour réparer.
  4. Un seul kernel installé (apt autoremove réflexe). Pas de fallback.
  5. oops=panic : tout oops kernel devient une panic complète immédiate.
  6. Hardening agressif : on optimise pour l'attaquant, pas pour l'opérateur en panique à 7 h du matin.

Aucun de ces choix n'est mauvais individuellement. Combinés, ils créent un système qui ne sait pas se rattraper.

La résolution (le piège de l'œuf et de la poule)

Toute la difficulté tient en une phrase : apt et dpkg dépendent de libc. Tant que libc est cassée, impossible de chrooter et de lancer apt --reinstall — tout segfault. La solution est d'extraire les .deb à la main depuis l'environnement de la live USB, qui a sa propre libc intacte.

1. Préparer les bind mounts

R=/mnt/sysroot
mount --bind /dev      "$R/dev"
mount --bind /dev/pts  "$R/dev/pts"
mount -t proc proc     "$R/proc"
mount -t sysfs sys     "$R/sys"
mount --bind /run      "$R/run"
cp -L /etc/resolv.conf "$R/etc/resolv.conf"

2. Faire de la place

Sur un système durci, beaucoup de gros logs peuvent dégager sans risque fonctionnel. Objectif : ≥ 500 Mo libres pour que les réinstallations puissent s'écrire sans re-saturer.

R=/mnt/sysroot
find "$R/var/cache/apt/archives" -name '*.deb' -delete
rm -rf "$R/var/cache/swcatalog/"*
rm -rf "$R/var/log/installer"
rm -f  "$R/var/log/"*.gz  "$R/var/log/"*.old
rm -rf "$R/var/log/journal/"*
find "$R/var/log/sudo-io" -mindepth 1 -delete
find "$R/var/log/pcp" -type f -delete
rm -f  "$R/var/log/audit/audit.log."*
truncate -s 0 "$R/var/log/audit/audit.log"

3. Télécharger les bons .deb dans la live, pas dans le chroot

cd /tmp
apt-get update
apt-get download libc6 libc-bin libc-l10n

# Backup de la libc corrompue, utile pour analyse forensique
cp /mnt/sysroot/usr/lib/x86_64-linux-gnu/libc.so.6 /tmp/libc.so.6.corrupted.bak

4. Extraire les .deb directement sur la cible

C'est l'étape clé. dpkg-deb -x écrase juste les fichiers, sans toucher à la base dpkg :

dpkg-deb -x /tmp/libc6_*.deb     /mnt/sysroot
dpkg-deb -x /tmp/libc-bin_*.deb  /mnt/sysroot
dpkg-deb -x /tmp/libc-l10n_*.deb /mnt/sysroot

# Vérif que libc.so.6 est de nouveau exécutable
$ /mnt/sysroot/usr/lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Debian GLIBC 2.41-12+deb13u3) stable release version 2.41.

5. Chroot, et réinstaller proprement via apt

chroot "$R" /bin/bash
/bin/true && echo "userspace OK"

apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install --reinstall -y \
    libc6 libc-bin libc-l10n

apt voit que les fichiers sont déjà là, mais cette fois il enregistre la version dans la base dpkg et exécute les scripts postinst. État cohérent retrouvé.

6. Audit complet via debsums

debsums vérifie les checksums MD5 fournis par chaque .deb pour tous les fichiers installés — bien plus complet que dpkg --verify qui ne couvre que les conffiles :

apt-get install -y debsums
debsums -ac 2>&1 | tee /tmp/debsums.out

Interprétation :

  • Fichiers /etc/* modifiés → normal, ce sont les conffiles modifiés par l'admin. Ne pas restaurer.
  • Fichiers /usr/lib/modules/* en échec ou manquants → critique, réinstaller le paquet linux-image-X.
  • Fichiers dans /usr/bin, /usr/lib, /usr/sbin en échec → critique, réinstaller le paquet propriétaire.

Dans ce cas précis : 7 modules kernel corrompus + btrfs.ko.xz MANQUANT. La saturation avait aussi touché les modules kernel. Rebooter en pensant en avoir fini avec la libc aurait produit une nouvelle panic sur le mount Btrfs.

apt-get install --reinstall -y linux-image-6.12.73+deb13-amd64

Ça régénère aussi l'initramfs automatiquement.

7. Régénérer GRUB, démonter proprement, rebooter

update-initramfs -u -k all
update-grub
exit

sync
umount -R /mnt/sysroot/sys /mnt/sysroot/proc /mnt/sysroot/dev
umount /mnt/sysroot/run /mnt/sysroot/boot/efi /mnt/sysroot
fsck.ext4 -f /dev/sdf2
fsck.vfat -a /dev/sdf1
reboot

Pas besoin de grub-install si l'EFI est intacte : le fallback /EFI/BOOT/BOOTX64.EFI + le shim fbx64.efi recréent l'entrée NVRAM au prochain boot.

Le rebondissement : la vraie cause racine

Trois mois plus tard, même scénario en préparation. Un check SSH montre :

/dev/sde2  3,4G  3,1G  107M  97% /    ← root à 97 %
load average: 8,54

PID 4595   root  /usr/bin/python3 /usr/bin/unattended-upgrade
PID 17266  root  /usr/bin/dpkg --unpack ... /tmp/apt-dpkg-install-dv8QKx
PID 343    md2_raid5     38.9% CPU
PID 820    md2_resync    16.7% CPU

unattended-upgrade tourne, dpkg unpacke, le RAID resync hammer l'I/O, plein de processus en D state. Trente secondes plus tard SSH refuse les connexions, puis plus aucun port TCP n'est joignable. Le ping tient encore : le kernel est vivant, userspace est figé. Sur la console :

EXT4-fs (sde2): failed to convert unwritten extents to written extents
                -- potential data loss! (inode XXXX, error -5)
EXT4-fs error (device sde2) in ext4_reserve_inode_write:5980: IO failure
[FAILED] Failed unmounting boot-efi.mount
[FAILED] Failed to start docker.service (×6)

error -5 = EIO, erreur d'entrée/sortie matérielle. Ce n'était pas la saturation disque : c'était la clé USB qui commençait à pourrir. La fatigue flash était la cause racine de février ; le disque plein n'avait été que la goutte d'eau.

Hard power-off au bouton, avec le risque de reproduire la corruption libc. Au reboot :

$ file /usr/lib/x86_64-linux-gnu/libc.so.6
ELF 64-bit LSB shared object [...]

libc OK, FS clean. La transaction dpkg en cours au moment du crash portait sur gpg / gpg-agent / dirmngr / gnupg-utils — aucun paquet critique. Si ça avait été libc ou systemd, retour à la live USB.

Les garde-fous à mettre en place

Ces mesures sont gratuites, prennent dix minutes, et reproduisent ce que tout sysadmin faisait avant qu'unattended-upgrades devienne assez transparent pour qu'on l'oublie.

1. Désactiver unattended-upgrades (le déclencheur)

sudo systemctl stop unattended-upgrades.service
sudo systemctl disable unattended-upgrades.service
sudo systemctl mask apt-daily.timer apt-daily-upgrade.timer

Sur un système fragile (petit disque, RAID en resync, hardening agressif), unattended-upgrades est un risque, pas une protection. Mises à jour à la main, en surveillant, quand la machine a de la marge.

2. apt-mark hold sur les paquets critiques

sudo apt-mark hold linux-image-6.12.73+deb13-amd64 linux-image-amd64 \
    libc6 libc-bin libc-l10n systemd systemd-sysv grub-efi-amd64

Ces paquets ne bougeront plus tant qu'on n'aura pas explicitement fait apt-mark unhold. Pour un kernel, ça veut dire tester la nouvelle version ailleurs d'abord et garder l'ancien comme fallback.

3. APT pre-invoke check d'espace disque

Un hook qui refuse toute transaction si la marge n'est pas là :

# /usr/local/bin/apt-disk-check.sh
#!/bin/bash
set -e
MIN_ROOT_MB=300
MIN_VAR_MB=300
MIN_CACHE_MB=500
PHASE="${1:-dpkg}"

free_mb() { df -BM --output=avail "$1" 2>/dev/null | tail -1 | tr -d ' M'; }

if [ "$PHASE" = "update" ]; then
  c=$(free_mb /var/cache/apt)
  [ -n "$c" ] && [ "$c" -lt "$MIN_CACHE_MB" ] && {
    echo "REFUS APT update: ${c}M libres sur /var/cache/apt" >&2; exit 1; }
else
  r=$(free_mb /); v=$(free_mb /var)
  [ -n "$r" ] && [ "$r" -lt "$MIN_ROOT_MB" ] && {
    echo "REFUS dpkg: ${r}M libres sur /" >&2; exit 1; }
  [ -n "$v" ] && [ "$v" -lt "$MIN_VAR_MB" ] && {
    echo "REFUS dpkg: ${v}M libres sur /var" >&2; exit 1; }
fi
// /etc/apt/apt.conf.d/99-disk-space-check
APT::Update::Pre-Invoke   { "/usr/local/bin/apt-disk-check.sh update"; };
DPkg::Pre-Invoke          { "/usr/local/bin/apt-disk-check.sh dpkg"; };

À noter : inliner le check awk directement dans le .conf avec des \" imbriqués ne passe pas le parseur APT. Un script externe est testable, debuggable, et n'embarque pas de syntaxe ésotérique dans une config.

4. Alerte espace disque < 25 % libre

# /usr/local/bin/disk-alert.sh
#!/bin/bash
THRESHOLD="${THRESHOLD:-75}"
MAIL_TO="${MAIL_TO:-root}"
HOST=$(hostname)
df --output=source,pcent,target -x tmpfs -x devtmpfs -x squashfs -x overlay -x ecryptfs \
  | tail -n +2 \
  | while read fs use mnt; do
      pct="${use%\%}"
      [ -z "$pct" ] && continue
      case "$pct" in (*[!0-9]*) continue;; esac
      if [ "$pct" -ge "$THRESHOLD" ]; then
        msg="[disk-alert] ${HOST}: ${fs} a ${pct}% (mount ${mnt})"
        logger -t disk-alert -p user.warning "$msg"
        command -v mail >/dev/null 2>&1 && echo "$msg" | mail -s "$msg" "$MAIL_TO" || true
      fi
    done
# /etc/cron.d/disk-alert
*/15 * * * * root THRESHOLD=75 MAIL_TO=admin@example.com /usr/local/bin/disk-alert.sh

Toujours pousser au moins en local via logger : même sans MTA configuré, l'info part dans journald.

5. Réactiver le mode recovery dans GRUB

sudo sed -i 's/^GRUB_DISABLE_RECOVERY=.*/GRUB_DISABLE_RECOVERY="false"/' /etc/default/grub

Le mode recovery donne un shell root single-user sans démarrer aucun service. C'est la différence entre 5 minutes de réparation et une soirée avec une live USB.

6. oops=panicpanic=30

Sur un serveur critique en datacenter, oops=panic force un redémarrage immédiat à la moindre warning kernel pour limiter une compromission. Sur un NAS perso, chaque oops bénin (driver USB capricieux) devient une panic complète.

# /etc/default/grub
# AVANT : ...debugfs=off oops=panic audit_backlog_limit=8192 panic=10
# APRÈS : ...debugfs=off audit_backlog_limit=8192 panic=30
sudo update-grub

panic=30 garde le bénéfice essentiel : auto-reboot après une vraie panic, avec 30 secondes pour lire le contexte sur la console.

7. Protéger les kernels contre apt autoremove

// /etc/apt/apt.conf.d/01autoremove-kernels
APT::NeverAutoRemove {
  "^linux-image-.*";
  "^linux-headers-.*";
  "^linux-modules-.*";
};

apt autoremove reste utile pour les paquets orphelins, mais ne pourra plus jamais virer un kernel. On en supprime à la main, en connaissance de cause.

Le vrai fix : virer la clé USB

Toutes les mesures ci-dessus sont des sparadraps. Une clé USB 4 Go en disque système, c'est du wearing flash, pas de SMART exploitable, un contrôleur cheap et zéro marge pour les updates.

  • Migrer vers un SSD M.2 ou SATA ≥ 64 Go (un 250 Go à 30 € fait le job, endurance ×100).
  • Avant migration : dd intégral de la clé courante (dd if=/dev/sdX of=/mnt/data/backup/nas-usb-$(date +%F).img bs=64M conv=fsync + gzip --best).
  • Snapshots Btrfs Snapper sur /mnt/data : gratuits en espace grâce au CoW, et btrfs send --proto 3 (nouveau en Debian 13) accélère l'envoi incrémental ×3.
  • Backup réel 3-2-1 : un RAID n'est pas un backup. Borgmatic + offsite, vérification mensuelle borg check --verify-data.
  • SMART monitoring sur les HDD (smartd + mails). Un RAID5 avec un disque qui meurt pendant un resync = perte totale, d'où la préférence pour RAID6 / mirror+stripe au-delà de 5 ans de disques.

Ce qu'il faut retenir

libc.so.6 corrompue = kernel panic immédiat sans signature claire dans le panic message. Le stack trace montre do_user_addr_fault → segfault dans PID 1, mais ne dit pas quelle bibliothèque est en cause. Inspection manuelle obligatoire.

Le hardening CIS protège contre les attaquants, mais réduit la tolérance aux pannes. Sur un NAS personnel, un compromis plus doux (recovery mode activé, anciens kernels conservés, oops=panic retiré) est plus sage que la baseline Level 2 stricte.

Les hooks APT de pré-check d'espace disque sont triviaux à mettre en place et sous-utilisés. Si vous ne retenez qu'une chose de cet article, mettez-en un en place ce soir.

debsums est l'outil de référence pour l'audit post-incident sur Debian, à installer par défaut sur tout serveur.

Et surtout : documentez vos incidents. Ce qui a sauvé du temps en mai, c'est d'avoir une trace écrite du diagnostic de février, au lieu d'avoir à le réinventer sous pression.

Références

PartagerLinkedInXBluesky

Articles similaires