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

·16 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.

Un matin, le NAS est mort

Routine matinale. Je consulte mon dashboard de monitoring. Un widget rouge. Mon NAS n'est pas joignable. Bon. J'essaie un ping, rien. SSH, rien. Je descends dans la pièce où il vit, je branche un clavier et un écran. Et là, GRUB me sort une phrase que je ne voulais pas voir :

Boot Option Restored

Suivi d'un grub> solitaire. Charmant. Je tape exit. Le menu GRUB normal revient. Mais avec une seule entrée kernel. Pas d'ancien noyau. Pas de mode recovery. Je sélectionne. Et là :

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

Suivi d'un beau stack trace :

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 qui meurt au tout premier exec userspace. Le kernel n'a pas le choix : il panique. Identique en single mode (que je n'ai même pas, j'y reviens). 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.

Bon. Café. On va avoir besoin.

Le contexte (parce que je l'ai cherché)

Mon NAS est un Terramaster F6-424 sur lequel j'ai remplacé le TOS d'origine par une Debian 13 (Trixie) installée sur la clé USB interne de 4 Go. Les 6 baies hébergent (entre autres) d'anciens disques Synology qui contiennent encore des structures RAID5 + LVM + Btrfs (vg1-volume_1) montées sur /mnt/data.

Le système est durci selon une baseline CIS / ANSSI :

  • AppArmor en mode enforce
  • auditd actif, logs sudo-io, PCP
  • Options kernel : oops=panic slab_nomerge init_on_alloc=1 init_on_free=1 page_alloc.shuffle=1 randomize_kstack_offset=on pti=on vsyscall=none debugfs=off
  • GRUB_DISABLE_RECOVERY="true" - pas de mode recovery dans le menu

J'écris ça maintenant comme si c'était neutre. Spoiler : presque chacun de ces choix va se retourner contre moi dans les heures qui viennent.

Diagnostic : boot live USB et debugfs

Je grave une Debian 13.5 XFCE sur une autre clé. Je boote dessus.

L'inventaire des disques (lsblk -f) confirme que le système est sur /dev/sdf (clé USB Flash 3,75 Gio) :

  • sdf1 : 243 Mo FAT32 → /boot/efi
  • sdf2 : 3,5 Go ext4 → /

Premier réflexe : fsck -n -f /dev/sdf2. Structure ext4 propre. Aucune erreur de filesystem. Donc la corruption n'est pas dans la structure. Elle est dans le contenu de fichiers.

Je sors debugfs, qui permet de lire un FS ext4 sans le monter (donc strictement 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 octets. Tronqués. Le .gz archivé contient l'historique antérieur. C'est là qu'on commence à voir la timeline qui pue.

Je monte enfin la partition pour pouvoir faire un chroot :

mount /dev/sdf2 /mnt/sysroot
mount /dev/sdf1 /mnt/sysroot/boot/efi
chroot /mnt/sysroot /lib/systemd/systemd

Réponse :

error while loading shared libraries:
/lib/x86_64-linux-gnu/libc.so.6: invalid ELF header

Et là, 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 bien, exec init (= systemd), le loader essaie de mapper libc, segfault, init meurt, panic.

À ce moment-là, j'ai juste envie de partir en weekend.

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.

Conclusion : 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 sont non écrits ou écrasés par autre chose. ELF invalide.

Pourquoi le crash était inéluctable

J'avais accumulé six décisions raisonnables prises isolément. Et catastrophiques combinées :

  1. Disque système de 3,5 Go. Une clé USB 4 Go pour un OS qui télécharge plusieurs centaines de Mo d'updates par mois.
  2. unattended-upgrade tournait sans surveillance, sans alerte mail, sans check d'espace pré-transaction.
  3. GRUB_DISABLE_RECOVERY="true" : pas de mode recovery dans le menu. Tu peux pas 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 = panic complète immédiate.
  6. Hardening agressif : tolérance aux pannes réduite. Logique : on optimise pour l'attaquant, pas pour l'opérateur en panique à 7h 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, je ne peux pas chrooter et lancer apt --reinstall. Tout segfault.

La solution, c'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 :

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"

Objectif : ≥ 500 Mo libres pour que les réinstallations puissent s'écrire sans re-saturer.

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

4. 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

5. 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.

Boom. Elle vit. Premier vrai sourire de la matinée.

6. 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à (extraction étape 5), mais cette fois il enregistre la version dans la base dpkg et exécute les scripts postinst. État cohérent retrouvé.

7. 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 (hardening, configs perso). 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 mon cas : 7 modules kernel corrompus + btrfs.ko.xz MANQUANT. La saturation avait aussi touché les modules kernel. Si j'avais rebooté en pensant en avoir fini avec la libc, le système aurait paniqué de nouveau sur le mount Btrfs. Réparation :

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

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

8. Régénérer GRUB et initramfs (filet de sécurité)

update-initramfs -u -k all
update-grub

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.

9. Démontage propre et reboot

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

Le NAS reboote. Login. uname -r. Tout est là. J'ai gagné. Ou plutôt, j'ai survécu.

Le rebondissement (mai 2026)

Trois mois plus tard. Fin de journée. Je consulte mon dashboard pour faire un suivi de santé du NAS. Premier check SSH :

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

Disque à 97 %, load à 8. Et un ps rapide montre ce qui tourne :

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 est en cours. dpkg unpacke. Le RAID resync hammer l'I/O. Plein de processus en D state (uninterruptible). C'est exactement le scénario de février qui se rejoue.

Trente secondes plus tard, SSH refuse les nouvelles connexions. Une minute après, plus aucun port TCP n'est joignable. Le ping tient encore, donc le kernel est vivant. Mais userspace est figé. Je vois sur la console (photo prise par mon collègue à côté du NAS) :

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 juste la saturation disque. C'était la clé USB qui commençait à pourrir. L'incident de février n'avait pas pour cause le disque plein. Il avait pour cause la fatigue flash. Le disque plein avait juste été la goutte d'eau.

J'avais passé trois mois à blâmer unattended-upgrade. Le vrai coupable se cachait derrière.

Pas eu le choix : hard power-off au bouton. Risque immense de reproduire la corruption libc. Reboot. Et là, miracle :

$ file /usr/lib/x86_64-linux-gnu/libc.so.6
ELF 64-bit LSB shared object [...]
$ /usr/lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Debian GLIBC 2.41-12+deb13u3) stable release version 2.41.

libc OK. FS clean. La transaction dpkg qui tournait au moment du crash : gpg / gpg-agent / dirmngr / gnupg-utils. Pas un paquet critique. Chance énorme. Si ça avait été libc ou systemd, c'était reparti pour la live USB.

Best practices 2026 que j'ai appliquées dans la foulée

Devant l'évidence qu'on a évité la balle deux fois en trois mois, j'ai appliqué une checklist immédiatement, sans attendre la migration matérielle. Toutes ces mesures sont gratuites, prennent dix minutes, et reproduisent ce que tout sysadmin sérieux faisait avant que unattended-upgrades devienne tellement transparent 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. Je préfère faire les mises à jour à la main, le week-end, en surveillant. C'est ma machine, je sais quand elle 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

Même si je relance apt upgrade un jour, ces paquets ne bougeront pas tant que je n'aurai pas explicitement fait apt-mark unhold. Pour un kernel, ça veut dire que je teste la nouvelle version sur une autre machine d'abord, je garde l'ancien comme fallback, je migre ensuite.

3. APT pre-invoke check d'espace disque

Le déclencheur racine était la saturation. Voici 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

Et la config APT correspondante :

// /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 : j'avais d'abord essayé d'inliner le check awk directement dans le fichier .conf avec des \" imbriqués. Le parseur APT n'aime pas. Mieux vaut un script externe : testable, debuggable, et ça n'embarque pas de syntaxe ésotérique dans une config.

4. Alerte espace disque < 25 % libre

Un cron toutes les 15 minutes :

# /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. Comme ça même sans MTA configuré, l'info part dans journald et peut être consultée plus tard. Le mail c'est le luxe.

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 te donne accès à un shell root single-user sans démarrer aucun service. Quand le système ne boote pas en mode normal mais peut chrooter, 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 sécurisé (datacenter, base bancaire, contexte d'exploitation kernel), oops=panic force un redémarrage immédiat à la moindre warning kernel pour limiter une compromission potentielle. Sur un NAS perso, c'est de la paranoïa contre-productive : chaque oops bénin (driver USB capricieux, warning bénin) devient une panic complète.

# Édition de /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, en laissant 30 secondes pour voir 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 faire le ménage des paquets orphelins, mais il ne pourra jamais virer un kernel. Même celui qui ne te sert plus. Tu en supprimes à la main quand tu sais ce que tu fais.

Ce qu'il reste à faire (et qui était l'évidence depuis le début)

Toutes les mesures ci-dessus sont des sparadraps. Le vrai fix, c'est de virer la clé USB. Une clé USB 4 Go en disque système, c'est jouer avec le feu : wearing flash, pas de SMART exploitable, contrôleur cheap, marge zéro pour les updates.

  • Migrer vers un SSD M.2 ou SATA ≥ 64 Go (Samsung 870 EVO 250 Go à 30 € fait le job, endurance ×100). Sur le F6-424 il y a un slot M.2 dédié, ce serait crime de ne pas l'utiliser.
  • Avant migration : dd intégral de la clé USB courante (dd if=/dev/sdX of=/mnt/data/backup/nas-usb-$(date +%F).img bs=64M conv=fsync + gzip --best). Point de restauration garanti.
  • 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 : /mnt/data est un RAID, pas un backup. Borgmatic + offsite (rsync.net, Hetzner Storage Box, Backblaze B2). 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. C'est pour ça que RAID6 / mirror+stripe sont préférables sur des disques de plus de 5 ans.

Ce que je retiens

libc.so.6 corrompue = kernel panic immédiat sans signature claire dans le panic message. Le trace stack montre do_user_addr_fault → segfault dans PID 1, mais ne dit pas QUELLE bibliothèque est en cause. Il faut inspecter à la main.

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 probablement plus sage que la baseline Level 2 stricte.

Les hooks APT pour pré-checker l'espace disque sont sous-utilisés alors qu'ils sont triviaux à mettre en place. Si vous n'avez qu'une seule chose à retenir 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. Installation par défaut sur tout serveur, c'est mon nouveau réflexe.

Une clé USB 4 Go en disque système n'est pas un choix, c'est un pari. Le ratio coût/risque d'un vrai SSD est imbattable.

Et surtout : ce qui m'a sauvé en mai, c'est d'avoir une mémoire écrite de l'incident de février. Quand le scénario s'est rejoué (même heure tardive, même fatigue, même tunnel de panique), je n'ai pas eu à réinventer le diagnostic. Documentez vos incidents. Faites-en des articles de blog. Le futur vous remerciera quand le NAS plantera à nouveau.

Références

PartagerLinkedInXBluesky

Articles similaires