RCE non authentifiée dans le backend RPC d'inférence distribuée de llama.cpp
Un contrôle de bornes manquant dans le backend RPC de llama.cpp permet à tout client ayant accès au port du serveur de lire et écrire la mémoire du processus et d'obtenir une exécution de code à distance. Corrigé dans b8492.
En bref La plupart des articles de sécurité LLM parlent de prompts. Celui-ci parle d’un socket réseau. llama.cpp embarque un backend RPC optionnel pour répartir l’inférence sur plusieurs machines. Un contrôle de bornes manquant dans l’analyse des tenseurs permet à tout client capable d’atteindre le port RPC de lire et d’écrire la mémoire du serveur, de contourner l’ASLR grâce à des fuites de pointeurs au niveau du protocole, et d’obtenir une exécution de code à distance — sans authentification, sans prompt, sans interaction utilisateur. L’avis du mainteneur (GHSA-j8rj-fmpv-wcxw) a été publié le 26 mars 2026 ; le correctif figure dans le build b8492. La documentation du projet indiquait déjà de ne pas exposer ce service sur un réseau — voici pourquoi.
De quoi s’agit-il ?
llama.cpp est le moteur C/C++ de référence pour exécuter localement des modèles à poids ouverts, avec bien plus de 100 000 étoiles sur GitHub. Au-delà de l’usage mono-machine, il propose un backend RPC d’inférence distribuée : on compile avec -DGGML_RPC=ON, on lance un ggml-rpc-server sur chaque hôte disposant d’un GPU, et un processus principal délègue les calculs de tenseurs via TCP. C’est ainsi que l’on fait tourner un modèle trop volumineux pour une seule machine sur plusieurs.
La faille est un bug de sûreté mémoire dans l’analyseur de requêtes du serveur RPC. Elle a été signalée au CERT/CC en février 2026, publiée sous forme d’avis de sécurité du mainteneur le 26 mars 2026, et notée CVSS 3.1 9.8 (critique), classée CWE-119 (restriction incorrecte des opérations dans les bornes d’un tampon mémoire). Elle affecte les builds antérieurs à b8492, où elle a été corrigée.
Comment ça marche
Lorsque le serveur RPC exécute un graphe de calcul, il reconstruit chaque tenseur à partir des octets reçus sur le réseau, via une fonction utilitaire. Celle-ci ne valide le pointeur data fourni par l’attaquant que si le champ buffer du tenseur est non nul. En positionnant buffer à 0, le contrôle de bornes est totalement contourné, tandis que le pointeur data est malgré tout repris directement depuis la requête et transmis aux noyaux de calcul :
// simplifié : buffer = 0 -> nullptr, donc le bloc de validation ci-dessous ne s'exécute jamais
result->buffer = reinterpret_cast<ggml_backend_buffer_t>(tensor->buffer);
if (result->buffer) {
// les contrôles de bornes / cohérence sur result->data ne vivent QUE dans ce bloc
}
result->data = reinterpret_cast<void *>(tensor->data); // repris de la requête sans condition
Cette seule brèche offre à l’attaquant une primitive de lecture/écriture arbitraire sur le processus serveur. Comme le protocole RPC renvoie aussi des adresses mémoire brutes lorsqu’un client alloue un tampon, nul besoin de deviner quoi que ce soit : les fuites de pointeurs contournent l’ASLR, et un pointeur de fonction situé dans la structure interne d’un tampon peut être écrasé pour rediriger une requête ultérieure vers une opération mémoire contrôlée par l’attaquant. Nous omettons délibérément une chaîne d’exploitation fonctionnelle — l’objet ici est la classe de bug, pas une arme prête à l’emploi. Le compte-rendu du rapporteur et l’avis contiennent les détails de reproduction pour les défenseurs qui en ont besoin.
Deux faits rendent la chose concrète et non théorique. D’abord, aucune authentification n’existe — le protocole n’en prévoit pas, donc l’accessibilité équivaut à la compromission. Ensuite, la même cause racine était derrière deux failles de lecture hors bornes corrigées dans cette fonction précise en 2024 ; ces correctifs avaient durci les commandes de lecture et d’écriture de tenseurs, mais la commande d’exécution de graphe emprunte un chemin de code distinct, jamais couvert. La faiblesse sous-jacente est présente depuis la première intégration du backend RPC en mai 2024.
Exemple de prompt
L’extrait ci-dessous montre la cause racine : le parseur de tenseurs RPC saute sa vérification de bornes quand buffer vaut 0, laissant un pointeur data contrôlé par l’attaquant filer vers les noyaux de calcul. Aucun exploit fonctionnel n’est montré — c’est pour les défenseurs.
# llama.cpp RPC graph-compute RCE (illustrative, defensive)
# The tensor parser only bounds-checks data when buffer != 0:
if tensor.buffer: # attacker sets buffer = 0 to skip this
validate(tensor.data) # [bounds check lives only here]
result.data = tensor.data # [payload] taken from the wire unconditionally
# Root cause: reachability == compromise; the RPC protocol has no auth.
# Defense: upgrade to build b8492+, bind ggml-rpc-server to 127.0.0.1,
# never publish port 50052, and tunnel nodes over mTLS/WireGuard.
Pourquoi c’est important
Un nœud d’inférence est une cible de grande valeur. Il détient des poids de modèle, des prompts en cours de traitement et souvent des clés d’API, et se trouve généralement sur un hôte GPU largement connecté au réseau interne. Une exécution de code sur un serveur RPC peut signifier un déplacement latéral dans un cluster, l’exfiltration de secrets et de modèles, ou tout simplement des GPU détournés. Le port d’écoute par défaut (50052) est facile à empreindre, et la raison même d’être du backend — l’inférence entre machines — pousse les opérateurs vers exactement l’exposition réseau qui transforme cela en exécution de code à distance. Les déploiements Docker qui publient le port ou l’écoutent sur 0.0.0.0 sont directement accessibles, souvent en root.
Défenses
- Mettre à jour vers le build b8492 ou ultérieur (le correctif, PR #20908), qui rétablit la validation des bornes sur le chemin d’exécution de graphe.
- Ne jamais exposer le port RPC sur un réseau non fiable. Le README RPC du projet est explicite : le backend est une preuve de concept, « fragile et non sécurisé », et ne doit jamais tourner sur un réseau ouvert. Traitez-le comme un service réservé à un LAN de confiance.
- Écouter sur
127.0.0.1et tunneliser. Pour une véritable inférence multi-hôtes, gardez chaqueggml-rpc-serversur la boucle locale et reliez les nœuds via un overlay mutuellement authentifié (WireGuard, tunnel mTLS, VPC privé) plutôt qu’en TCP brut sur une interface routable. - Ne pas lancer les conteneurs d’inférence avec
--network=host, et ne jamais publier 50052 sur Internet. N’exposez que l’API HTTP prévue, derrière une authentification. - Segmenter et surveiller. Placez les nœuds d’inférence GPU dans une zone réseau isolée ; alertez sur les connexions inattendues vers les ports RPC et sur les processus d’inférence qui lancent des shells.
- Rechercher l’exposition. Scannez vos propres plages pour repérer les ports RPC accessibles et confirmez qu’aucun ne répond depuis l’extérieur de la frontière de confiance.
Statut
| Élément | Détail |
|---|---|
| Référence | CVE-2026-34159 · GHSA-j8rj-fmpv-wcxw · CWE-119 |
| Affecté | Backend RPC de llama.cpp, builds antérieurs à b8492 |
| Sévérité | CVSS 3.1 9.8 (critique) — réseau, non authentifié, sans interaction |
| Signalé | CERT/CC, 8 févr. 2026 (VU#748698) |
| Avis | Publié le 26 mars 2026 |
| Correctif | Build b8492 (PR #20908) |
| Antérieurs liés | Même fonction, deux corrections de lecture hors bornes en 2024 (CVE-2024-42478 / CVE-2024-42479) |
| Note | Le backend RPC est documenté comme non sécurisé et hors du périmètre de vulnérabilité du projet |
Sources
- → https://nvd.nist.gov/vuln/detail/CVE-2026-34159
- → https://github.com/ggml-org/llama.cpp/security/advisories/GHSA-j8rj-fmpv-wcxw
- → https://github.com/ggml-org/llama.cpp/pull/20908
- → https://github.com/ggml-org/llama.cpp/blob/master/tools/rpc/README.md
- → https://www.sentinelone.com/vulnerability-database/cve-2026-34159/