SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-21

Binaire Linux incompatible : glibc ou musl ?

L’erreur « GLIBC_2.34 not found » vient de l’hôte de build. Découvrez ce que le binaire exige et 4 méthodes pour le distribuer sur des systèmes plus anciens.

Pourquoi un binaire Linux ne s’exécute pas sur un serveur plus ancien

Produire un binaire Linux portable est difficile, car glibc, la bibliothèque C de GNU, garantit la compatibilité dans une seule direction. Un ancien binaire continue de fonctionner avec une nouvelle version de glibc. Un nouveau binaire ne fonctionne pas avec une ancienne version de glibc. La machine sur laquelle vous compilez détermine la version minimale requise par toutes les machines sur lesquelles vous déployez le binaire.

L’erreur indique la version exacte requise :

./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)

Le fichier n’est pas corrompu et la configuration n’est pas incorrecte. Le chargeur dynamique lit une balise de version enregistrée dans le binaire, recherche cette balise dans la libc du système, ne la trouve pas et refuse de démarrer le programme. Télécharger à nouveau le fichier et exécuter chmod +x ne change rien, car cette exigence est enregistrée dans le fichier lui-même. Vous devez modifier la méthode de compilation du binaire ou modifier ce que vous distribuez.

Ce que fait réellement le versionnage des symboles glibc

Chaque fonction exportée par glibc possède une étiquette de version. printf dans libc.so.6 est en réalité printf@@GLIBC_2.2.5. Lorsque glibc modifie le comportement ou l’ABI (interface binaire d’application) d’une fonction, elle ne remplace pas l’ancienne. Elle conserve l’ancien code sous l’ancienne étiquette et ajoute le nouveau code sous une nouvelle étiquette. Un seul libc.so.6 contient donc plusieurs versions du même symbole. Un binaire de 2009 retrouve toujours l’étiquette de 2009, ce qui explique pourquoi la compatibilité ascendante fonctionne si bien.

L’étape d’édition des liens enregistre ce qu’elle a utilisé. Votre binaire reçoit une section .gnu.version_r qui indique en pratique : « J’ai besoin de GLIBC_2.38 dans libc.so.6 ». Une ancienne libc ne possédait jamais cette étiquette. Le chargeur s’arrête donc avant l’exécution de main. Il n’existe aucun mécanisme de repli, car cela reviendrait à fournir silencieusement au programme une fonction différente de celle contre laquelle il a été compilé.

Le déclencheur le plus fréquent depuis 2021 est glibc 2.34. Cette version a fusionné libpthread et libdl dans libc et a déplacé __libc_start_main, la fonction qui démarre chaque programme C, vers l’étiquette GLIBC_2.34. Un programme hello-world compilé sur n’importe quelle distribution avec glibc 2.34 ou une version ultérieure exige donc GLIBC_2.34, même si le code source n’appelle aucune fonction récente. C’est pourquoi le problème est apparu soudainement chez des personnes dont le code n’avait pas changé depuis des années.

Quelle version de glibc ma distribution fournit-elle ?

Chartglibc version in each distribution's own repositories
The data behind this chart
[
  {
    "distro": "CentOS 7",
    "glibc": 2.17
  },
  {
    "distro": "RHEL 8",
    "glibc": 2.28
  },
  {
    "distro": "Ubuntu 20.04",
    "glibc": 2.31
  },
  {
    "distro": "Debian 11",
    "glibc": 2.31
  },
  {
    "distro": "RHEL 9",
    "glibc": 2.34
  },
  {
    "distro": "Ubuntu 22.04",
    "glibc": 2.35
  },
  {
    "distro": "Debian 12",
    "glibc": 2.36
  },
  {
    "distro": "Ubuntu 24.04",
    "glibc": 2.39
  },
  {
    "distro": "Debian 13",
    "glibc": 2.41
  }
]

Voici les versions que chaque distribution fournit dans ses propres dépôts, selon les informations publiées par les distributions et à jour en août 2026. CentOS 7 a atteint sa fin de vie depuis longtemps, mais reste dans la liste parce que des serveurs hérités l’exécutent encore. Vérifiez n’importe quelle machine dont vous disposez avec ldd --version, qui affiche la release de glibc sur sa première ligne, ou avec getconf GNU_LIBC_VERSION.

Lisez ces 9 lignes comme une échelle. Compilez sur Debian 13 avec glibc 2.41 : le résultat ne fonctionnera pas sur une version antérieure à Debian 13. Compilez sur Ubuntu 20.04 avec glibc 2.31 : le même code source fonctionnera sur toutes les lignes situées au-dessus, y compris la machine Debian 13. Le serveur le plus ancien que vous devez prendre en charge est le seul hôte de compilation qui compte. La distribution que vous standardisez définit donc le niveau minimal d’ABI pour tout ce que vous compilez pour ce parc. Il est préférable de prendre cette décision avant de déployer le parc, en même temps que les autres arbitrages liés au choix du système d’exploitation à utiliser sur votre VPS.

Comment trouver la version minimale de glibc requise par un binaire ?

Lisez-la directement dans le fichier. Cette méthode fonctionne dans tous les cas, y compris avec un binaire fourni par un éditeur sans documentation de compilation.

objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5

La dernière ligne indique la version minimale. Si objdump est absent, installez le paquet binutils. Si vous ne pouvez rien installer sur cette machine, strings -a ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 donne un résultat approchant, car les tags sont stockés sous forme de chaînes de caractères dans le fichier.

readelf -V ./mytool affiche la même exigence de manière structurée. Recherchez le bloc intitulé Version needs section '.gnu.version_r'. Il contient une ligne Name: GLIBC_x.y par tag, regroupée sous la bibliothèque qui doit le fournir.

Pour déterminer quelle fonction impose cette version minimale, recherchez le tag avec grep : objdump -T ./mytool | grep GLIBC_2.38. Il s’agit souvent d’un seul symbole, parfois évitable. Si objdump -T n’affiche absolument rien, le binaire est lié statiquement. Il ne possède donc pas de table des symboles dynamiques ni d’exigence de version à afficher.

Ce que file et readelf -d vous apprennent sur un binaire qu’on vous a remis

file indique l’architecture et le type de liaison sur une seule ligne.

file ./mytool

Un build glibc classique se présente ainsi :

ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., for GNU/Linux 3.2.0, not stripped

Un build statique indique statically linked et ne nomme aucun interpréteur. Un build musl en nomme un autre :

ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, ...

L’interpréteur est le champ important. Il s’agit du chargeur dynamique que le kernel exécute avant votre programme. Ce chemin exact doit exister sur la machine cible. /lib/ld-musl-x86_64.so.1 n’est pas présent sur un serveur Debian ou Ubuntu, sauf si quelqu’un y a installé musl.

readelf -d ./mytool répertorie les bibliothèques partagées dont le binaire a besoin, par nom :

Dynamic section at offset 0x2d58 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)   Shared library: [libssl.so.3]
 0x0000000000000001 (NEEDED)   Shared library: [libc.so.6]
 0x000000000000001d (RUNPATH)  Library runpath: [$ORIGIN/../lib]

Chaque entrée NEEDED est une exigence stricte, résolue par son soname. libssl.so.3 correspond à OpenSSL 3. Le binaire ne se chargera donc pas sur un serveur qui ne possède que libssl.so.1.1. Le soname l’explique : un soname différent indique un ABI volontairement incompatible. RUNPATH indique au chargeur où chercher en premier, et $ORIGIN se développe en répertoire contenant le binaire. C’est ainsi qu’un bundle autonome trouve ses propres bibliothèques.

N’exécutez pas ldd sur un binaire auquel vous ne faites pas confiance. Avec glibc, ldd peut résoudre les dépendances en exécutant le programme sous le chargeur. Un fichier malveillant peut donc exécuter du code. readelf et objdump se contentent de lire les octets. Avant toute chose, vérifiez que le téléchargement correspond bien au fichier publié par le projet, car vérifier un téléchargement avec la somme de contrôle publiée est la seule étape qui permet de savoir de qui proviennent les octets que vous détenez.

Les erreurs que vous verrez réellement et leur signification

Le loader indique une version de GLIBC introuvable. Le binaire a été compilé avec une version de glibc plus récente que celle de la cible. Aucun paquet installé sur la cible ne permet de corriger ce problème de manière sûre. Choisissez l’une des quatre options ci-dessous.

Le shell affiche No such file or directory pour un fichier clairement visible. L’élément manquant est l’interpréteur, pas le binaire. execve renvoie ENOENT lorsque le chemin du loader est absent de l’en-tête ELF, et le shell affiche le seul message dont il dispose. Exécutez file et lisez le chemin de l’interpréteur. Un binaire lié à musl sur un serveur uniquement équipé de glibc produit exactement ce symptôme.

cannot execute binary file: Exec format error signifie que l’architecture est incorrecte : un binaire x86-64 sur un serveur arm64, ou l’inverse. La première ligne de la sortie de file indique l’architecture du fichier.

error while loading shared libraries: libssl.so.3: cannot open shared object file signifie qu’une bibliothèque NEEDED est absente ou disponible sous un autre soname. Installez le paquet de la distribution correspondant. Les noms de paquets diffèrent selon les familles de distributions. Traduisez donc la commande avant de copier une ligne apt depuis un README. Les équivalents des commandes dnf et apt indiquent cette correspondance.

Un simple Segmentation fault provenant d’un build musl qui fonctionne correctement avec glibc. Il s’agit généralement de la taille de la pile des threads, présentée dans l’option 2.

Quatre façons de distribuer un binaire Linux portable

Chaque solution a un coût réel. Choisissez-la en fonction du comportement de votre programme à l’exécution, et non de celle qui semble la plus propre.

Option 1 : compiler sur la distribution la plus ancienne prise en charge

La réponse la moins intéressante, mais généralement la bonne. Compilez dans une image de conteneur de la distribution la plus ancienne que vous vous engagez à prendre en charge. L’éditeur de liens ne peut enregistrer que les tags réellement présents dans cette ancienne version de glibc. Le niveau minimal correspond donc à cette version, tandis que le binaire reste un binaire dynamique classique et conserve tout le comportement de glibc.

docker run --rm -v "$PWD:/src" -w /src ubuntu:20.04 sh -c 'apt-get update && apt-get install -y build-essential && make'

Le résultat fonctionne avec glibc 2.31 et toutes les versions ultérieures. Vérifiez au lieu de supposer : exécutez la commande objdump -T sur une seule ligne, présentée plus haut, avec le résultat, puis vérifiez que le tag le plus élevé correspond à celui attendu.

Le coût concerne la toolchain. Une ancienne image de base contient également un ancien compilateur, ce qui pose problème lorsque votre code nécessite une version récente de la norme C++. Go et Rust évitent généralement cette contrainte, car leurs toolchains s’installent indépendamment des paquets de la distribution, directement dans le conteneur. Pour C et C++, vous pouvez ajouter un compilateur plus récent depuis le canal toolchain de la distribution, ou utiliser l’une des images manylinux, conçues précisément pour associer une glibc ancienne à un GCC récent. Le compilateur C fourni avec Zig peut également cibler directement une version choisie de glibc, comme dans zig cc -target x86_64-linux-gnu.2.28. Vous obtenez ainsi le même niveau minimal sans conserver une ancienne image.

Option 2 : établir une liaison statique avec musl

musl est une petite bibliothèque C conçue pour la liaison statique. Un binaire musl statique contient sa propre libc, ne déclare aucun interpréteur et s’exécute avec tout noyau Linux de la bonne architecture. C’est ainsi que sont compilés la plupart des outils distribués sous forme de fichier unique.

sudo apt install -y musl-tools
musl-gcc -static -O2 hello.c -o hello
file ./hello

file devrait maintenant indiquer statically linked, sans champ d’interpréteur. Avec Rust, ajoutez la cible et compilez pour celle-ci :

rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl

Go n’a besoin de rien de tout cela. Avec CGO_ENABLED=0 go build, le résultat est déjà statique et n’utilise aucune libc.

Les recherches NSS changent. glibc résout les utilisateurs et les noms d’hôte via NSS (name service switch), qui charge les modules libnss_* avec dlopen pendant l’exécution du programme. Une glibc liée statiquement ne peut pas le faire, et le linker affiche cet avertissement :

warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

musl évite cet avertissement en n’implémentant pas NSS. Elle possède son propre resolver et lit directement /etc/resolv.conf et /etc/hosts. Sur un VPS ordinaire, c’est suffisant et plus simple. Sur un hôte où les comptes ou les noms proviennent de LDAP ou de SSSD, votre binaire ne les verra pas alors que tous les autres programmes du serveur y auront accès. Le resolver de musl est également plus récent que celui de glibc : le fallback TCP pour les réponses DNS de plus de 512 octets a été ajouté dans musl 1.2.4 en 2023. Les builds utilisant une version plus ancienne de musl tronquent donc les réponses volumineuses.

dlopen ne fonctionne pas. Dans un binaire musl statique, dlopen est un stub qui échoue toujours. Tout ce qui charge du code à l’exécution est exclu : systèmes de plugins, modules PAM, pilotes GPU et propres modules de jeux de caractères iconv de glibc. Si votre programme a besoin de dlopen, la liaison statique est impossible et vous devez choisir l’une des trois autres solutions.

Les mises à jour de sécurité deviennent votre responsabilité. Un binaire lié dynamiquement récupère un correctif libc dès que le serveur exécute sa mise à jour des paquets. Un binaire statique ne le fait jamais. Lorsqu’une entrée CVE (common vulnerabilities and exposures) concerne votre libc, ou une version statique d’OpenSSL que vous avez intégrée, vous devez recompiler et redistribuer le binaire. Chaque copie déjà déployée reste vulnérable jusqu’à ce qu’un administrateur remplace le fichier. Conservez la liste des composants liés au binaire, car rien sur la cible ne peut indiquer à un administrateur que votre binaire à fichier unique contient une bibliothèque datant de deux ans.

La licence change. glibc est publiée sous licence LGPL, et la liaison statique déclenche l’obligation de relicence prévue par la LGPL : vous devez fournir aux destinataires ce dont ils ont besoin pour relier votre programme à une autre version de glibc. musl est publiée sous licence MIT et n’impose pas cette condition. C’est la principale raison pour laquelle les projets qui distribuent des binaires à fichier unique choisissent musl plutôt que glibc en liaison statique.

Deux différences d’exécution provoquent des plantages difficiles à comprendre. La pile par défaut d’un thread musl est de 128 KiB, contre 8 MiB avec glibc. Le code qui place un buffer volumineux sur la pile d’un thread provoque donc un segfault sans message ni ligne dans les journaux. Définissez explicitement la taille avec pthread_attr_setstacksize, ou déplacez le buffer dans le heap. L’allocator de musl est également conçu pour les petites tailles et un comportement prévisible, plutôt que pour de nombreux threads qui allouent simultanément. Les programmes multithread fortement consommateurs d’allocations peuvent donc être sensiblement plus lents. Mesurez les performances avec votre propre charge de travail au lieu de vous fier à la réputation de l’une ou l’autre bibliothèque.

Option 3 : intégrer le loader et les bibliothèques

Livrez les bibliothèques à côté du binaire avec le loader correspondant, puis démarrez le programme via ce loader.

./ld-linux-x86-64.so.2 --library-path ./lib ./mytool

Pour rendre ce choix permanent, écrivez les chemins dans le fichier avec patchelf :

patchelf --set-interpreter "$PWD/lib/ld-linux-x86-64.so.2" --set-rpath '$ORIGIN/lib' ./mytool

Une seule règle détermine si cette méthode fonctionne : le loader et libc.so.6 doivent provenir de la même build de glibc. Ils forment une paire indissociable. Mélanger le loader de l’hôte avec une libc intégrée provoque un crash au démarrage au lieu d’afficher une erreur compréhensible. Intégrez les deux, ou n’en intégrez aucun.

AppImage est une mise en paquet de ce modèle. Le payload se trouve dans une image squashfs, et un petit runtime la monte. Il faut retenir une caractéristique : une AppImage n’intègre pas glibc. Une AppImage construite sur une distribution récente échoue donc avec la même erreur de version sur un ancien serveur. Les recommandations d’AppImage indiquent de construire sur la base la plus ancienne prise en charge. AppImage constitue donc un format de distribution qui s’appuie sur l’option 1, et non un remplacement de celle-ci.

Sur un serveur headless, AppImage a besoin de FUSE (filesystem in userspace) pour monter son payload. Une image VPS minimale n’inclut souvent pas ce composant. L’échec mentionne libfuse.so.2. Vous pouvez éviter complètement le montage avec ./App.AppImage --appimage-extract-and-run. Cette option extrait le contenu dans un répertoire temporaire, puis l’exécute depuis ce répertoire.

Option 4 : distribuer une image de conteneur

Déplacez tout l’espace utilisateur avec le programme. L’image contient sa propre libc. La glibc de l’hôte n’a donc plus d’importance. Seuls le noyau et l’architecture doivent être compatibles. C’est l’option la moins sophistiquée. Elle supprime toute cette catégorie de problèmes, ce qui explique pourquoi de nombreux logiciels serveur sont distribués de cette manière. Exécuter Docker sur un VPS est la méthode habituelle pour l’utiliser.

Le noyau de l’hôte impose toutefois toujours une limite. Les versions récentes de glibc utilisent de nouveaux appels système. Un ancien hôte de conteneurs peut les bloquer : glibc 2.34 et les versions ultérieures utilisent clone3, que les profils seccomp par défaut plus anciens refusent. Le symptôme est un échec immédiat avec Operation not permitted, sans aucune mention de glibc. Mettre à niveau le runtime de conteneurs sur l’hôte corrige le problème, car le blocage se trouve dans le filtre d’appels système du runtime, et non dans le noyau.

Les coûts sont classiques. La cible doit disposer d’un runtime de conteneurs et des permissions nécessaires pour l’utiliser. Le téléchargement passe d’un seul fichier à plusieurs dizaines ou centaines de mégaoctets. Vous devez désormais gérer une image de base et son calendrier de correctifs. Le travail de sécurité évité avec l’option 2 revient donc sous la forme de reconstructions d’image. Pour un service qui fonctionne en continu, ce compromis est raisonnable. Pour un outil en ligne de commande exécuté une seule fois, il ne l’est pas.

Quelle option choisir ?

  • Un outil interne destiné à des serveurs que vous contrôlez et qui utilisent tous la même distribution : compilez dynamiquement sur cette distribution, puis arrêtez-vous là.
  • Un fichier unique que des personnes externes téléchargent et exécutent : utilisez musl en liaison statique si le programme n’a besoin ni de dlopen ni de recherches basées sur NSS.
  • Un programme avec des plugins ou un accès au GPU : restez en liaison dynamique, compilez sur une ancienne base et utilisez l’option 3 pour le distribuer.
  • Un service de longue durée sur une machine qui dispose déjà d’un runtime : distribuez l’image.

Quel que soit votre choix, documentez-le et vérifiez-le. La glibc de l’hôte de build fait désormais partie de votre processus de release. Une machine de build mise à niveau d’une version LTS à la suivante augmente silencieusement le niveau minimal requis et fait fonctionner votre programme chez des utilisateurs qui n’avaient aucun problème le mois dernier. Épinglez l’image de build avec un tag et vérifiez dans la sortie le tag GLIBC_ le plus élevé dans le cadre du build. Le contrôle échouera ainsi dans votre pipeline plutôt que dans le terminal de quelqu’un d’autre.

FAQ

Pourquoi version GLIBC_2.38 not found s’affiche-t-il sur mon serveur ?

Le binaire a été compilé sur une machine équipée d’une version de glibc plus récente que celle de votre serveur. Le versionnage des symboles de glibc assure uniquement la compatibilité vers l’avant : les anciens binaires s’exécutent avec une nouvelle glibc, mais les nouveaux binaires ne s’exécutent pas avec une ancienne glibc, car le loader a besoin de la version exacte enregistrée dans le fichier et une ancienne libc ne possédait jamais ce tag. Rien de ce que vous installez sur le serveur ne corrige ce problème de manière sûre. Recompilez avec une base plus ancienne, fournissez un build statique avec musl, embarquez les bibliothèques avec leur loader correspondant ou fournissez une image de conteneur.

Comment savoir quelle version de glibc un binaire nécessite-t-il ?

Lisez les tags de version du fichier avec objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5. La dernière ligne indique la version minimale de glibc capable de le charger. readelf -V ./mytool affiche les mêmes exigences dans la section .gnu.version_r, regroupées par bibliothèque qui doit les fournir. Si objdump -T n’affiche rien, le binaire est statique et ne nécessite aucune glibc. Comparez le résultat avec la version présente sur votre serveur, obtenue avec ldd --version.

Un binaire musl est-il plus lent qu’un binaire glibc ?

Cela dépend de la charge de travail. La seule réponse fiable consiste à mesurer. Un binaire musl statique démarre plus rapidement, car aucun loader ne doit être exécuté et aucun travail de relocation n’est nécessaire au moment de l’exécution. En revanche, l’allocator de musl est conçu pour être compact et avoir un comportement prévisible, plutôt que pour gérer de nombreux threads qui allouent de la mémoire simultanément. musl fournit également moins de routines de chaînes et de mémoire optimisées manuellement que glibc. Les programmes multithread qui effectuent beaucoup d’allocations ou de traitements de chaînes peuvent donc être sensiblement plus lents. Mesurez votre propre programme sur votre propre serveur avant de retenir l’une ou l’autre affirmation.

Pourquoi bash affiche-t-il No such file or directory pour un fichier qui existe ?

Le fichier manquant est le dynamic loader, et non votre binaire. Le kernel lit le chemin de l’interpréteur dans l’en-tête ELF et renvoie ENOENT lorsque ce chemin est absent. Le shell l’affiche avec le seul message dont il dispose. Exécutez file ./mytool et lisez le champ interpreter. S’il indique /lib/ld-musl-x86_64.so.1 sur un serveur Debian ou Ubuntu, vous utilisez un binaire lié avec musl sur un système glibc. Vous devez donc télécharger la version compatible avec musl ou utiliser un build statique.

Puis-je copier libc.so.6 depuis un serveur plus récent pour corriger ce problème ?

Non. libc.so.6 et ld-linux-x86-64.so.2 forment une paire indissociable issue d’un même build de glibc, et tous les processus de la machine les utilisent. Remplacer la copie système peut empêcher le serveur d’exécuter quoi que ce soit, y compris les outils nécessaires pour annuler la modification. Si vous devez exécuter un binaire plus récent sur un ancien hôte, décompressez la nouvelle glibc dans un répertoire privé et démarrez ce programme avec son propre loader à l’aide de --library-path. Cette méthode n’affecte que ce processus. Recompiler avec l’ancienne glibc reste la solution la plus prévisible à long terme.

#glibc#musl#static-linking#portability#binaries