Blog / CVE expliquées

LMCache CVE-2026-105192 : l’exécution avant le rejet

LMCache CVE-2026-105192 atteint pickle pendant le décodage des messages. Suivez le correctif, les appelants du transport et les preuves nécessaires pour faire confiance à un test de non-régression négatif.

Études doctorales en sécurité de l’information achevéesSoutenance universitaire réussie · Soutenance finale de thèse à venir

La requête a échoué. Le code s’était déjà exécuté.

La requête a échoué. Le code s’était déjà exécuté. C’est le problème d’ordre dans LMCache CVE-2026-105192 : un désérialiseur non sûr s’exécute pendant le décodage des arguments, avant que le gestionnaire métier n’ait l’occasion de les rejeter. Une réponse d’erreur arrive trop tard pour prouver que la requête était inoffensive.

L’avis couvre LMCache de la 0.3.9 à la 0.5.5, les préversions concernées de la 0.5.6 et les builds de développement antérieurs au correctif. La version 0.5.6 contient la réparation. Il s’agit d’une revue du code source et du correctif, pas d’une reproduction ; la distinction compte surtout pour les tests négatifs évoqués plus loin. L’exposition concerne le transport multiprocessus vulnérable et atteignable, pas toute application qui utilise vLLM. [1][2]

Comment le décodage MessagePack atteint-il pickle ?

Le transport accepte MessagePack, mais son hook d’extension personnalisé donne à certains octets une seconde interprétation. Le code d’extension 1 aboutit à DeviceIPCWrapper.Deserialize, dont l’implémentation vulnérable appelle pickle.loads. Dans le chemin REGISTER_KV_CACHE, cela se produit alors que le serveur construit encore les arguments que recevra le gestionnaire. Vérifier ces arguments ensuite ne peut pas annuler un effet de bord déjà survenu. [1][3]

Voici comment je suivrais la frontière lors d’une revue : partir du décodeur réseau, recenser les hooks et les convertisseurs personnalisés, puis suivre chacun jusqu’à ce que les octets deviennent des données ordinaires ou une reconstruction exécutable. Le format qui figure sur un schéma d’architecture ne suffit pas. Un format externe orienté données peut contenir une extension qui rétablit tout le comportement dangereux que la couche externe semblait éviter.

Affected decode path (schematic)

Peer-controlled frame
  -> MessagePack argument decode
  -> extension code 1
  -> DeviceIPCWrapper.Deserialize
  -> pickle.loads
  -> only then: handler / later validation

A later error cannot undo an earlier side effect.

Le développeur avait besoin de retrouver la bonne sous-classe

Le code vulnérable explique son objectif d’ingénierie : conserver le type concret du wrapper de périphérique d’un bout à l’autre, pour que le récepteur aiguille vers la bonne implémentation de conversion de tenseurs. Les wrappers CPU et accélérateur ne font pas le même travail. Sérialiser l’objet résolvait commodément cet aiguillage, mais permettait aussi à un pair de fournir des octets à la reconstruction d’objets Python. L’erreur de sécurité réside dans la représentation choisie pour un besoin légitime. [3]

La question de revue est donc plus utile qu’une interdiction générale de pickle : quelles informations doivent franchir la frontière, et quels choix d’implémentation doivent rester locaux ? Un récepteur peut avoir besoin d’un type de périphérique, d’une forme ou d’un descripteur de stockage. Il n’a pas besoin d’accepter une recette arbitraire de construction d’objets Python. Écrire cette distinction donne au correctif un invariant qui survit à un changement de transport.

Le correctif ramène le choix de l’implémentation dans le code local

Dans la 0.5.6, Deserialize décode un descripteur typé WireIPCData, puis utilise son type pour choisir une entrée de WRAPPER_TYPES. Le choix du module et de la classe provient de cette table de correspondance locale. La présence d’importlib dans le chemin corrigé n’est pas, à elle seule, une nouvelle injection : la vraie question est de savoir si l’entrée contrôle le nom importé ou se contente de sélectionner un descripteur explicitement pris en charge. [4]

Le correctif conserve l’interface de sérialisation existante mais rejette les anciennes données pickle. Mon critère d’acceptation est que des données réseau malformées ou non prises en charge échouent avant toute reconstruction d’objet arbitraire, sans branche de compatibilité qui retenterait pickle en silence. Lors d’une revue, je testerais des étiquettes inconnues, des champs inattendus et d’anciens descripteurs, en plus des entrées valides prises en charge. Un correctif de sécurité doit préserver le travail prévu tout en supprimant l’interprétation dangereuse.

# Selected source lines; surrounding checks omitted [3][4]
# Vulnerable:
return pickle.loads(data)

# Fixed:
descriptor = msgspec.msgpack.decode(data, type=WireIPCData)
module, name = WRAPPER_TYPES[type(descriptor)]

# The fixed path selects implementation from a local mapping.

Suivre le décodeur partagé au-delà du premier socket

ZeroMQ n’est pas le seul appelant à examiner. La PR du correctif identifie un codec gRPC qui utilise le même désérialiseur au travers d’un champ nommé pickled_payload. Ce nom de champ subsiste pour des raisons de compatibilité, même si son contenu devient le nouveau descripteur. Un grep sur l’ancien nom de champ peut surestimer le danger ; un grep limité au premier transport peut manquer la frontière partagée. Suivez le flux de données réel. [5]

Le correctif est aussi plus limité que la suppression de tout usage de pickle dans LMCache. Les mainteneurs identifient d’autres chemins pilotés par le moteur, hors du champ de cette réparation, et les décrivent comme désactivés par défaut et non atteignables par le réseau dans cette configuration. C’est leur déclaration de périmètre, pas un résultat d’atteignabilité démontré ici. Le correctif n’établit ni l’authentification RPC ni l’autorisation d’utiliser une ressource de mémoire partagée référencée. Syntaxe, identité et propriété des ressources sont des contrôles distincts. [5][6]

Un descripteur valide ne prouve pas la propriété de la ressource

Le nouveau schéma fait davantage que changer de bibliothèque de sérialisation. TensorMetadata contraint les champs numériques et vérifie que la forme et les pas ont des rangs identiques ; le descripteur de mémoire partagée CPU vérifie que la vue de tenseur décrite tient dans la taille annoncée. Ces contrôles encadrent ce que signifie un descripteur avant qu’un wrapper ne l’utilise. Ce sont des propriétés concrètes qu’un relecteur peut transformer en cas limites, plutôt que d’accepter le mot « typé » comme une garantie de sécurité. [7]

Une question distincte demeure : qui est autorisé à nommer ou à utiliser la ressource sous-jacente ? Une description cohérente en interne n’y répond pas à elle seule. La PR sépare explicitement la validation du descripteur de l’autorisation liée à la propriété de la mémoire partagée. Je consignerais le contrôle de propriété de la ressource et son emplacement séparément du contrôle de décodage, et j’exigerais des preuves pour les deux là où le modèle de confiance du système en a besoin. Ce n’est pas l’annonce d’une nouvelle vulnérabilité dans la version corrigée ; c’est la limite de ce que ce correctif établit. La même rigueur s’applique à tout correctif de parseur : énumérez l’invariant qu’il impose désormais, puis identifiez les décisions de sécurité indépendantes qu’il n’a jamais promis de prendre. Un test de parseur réussi ne peut pas certifier ces décisions par association. [5]

L’absence d’un marqueur exige de prouver que le traitement est terminé

Un test peut signaler un échec avant que le serveur vulnérable n’ait traité son entrée. L’auteur de la PR traite explicitement ce problème de synchronisation asynchrone : une requête de suivi sur la même connexion établit que le traitement est terminé avant l’examen du marqueur. Le résultat avant/après rapporté appartient à cet auteur ; ce n’est pas un résultat de mon lab. La leçon générale est de prouver que la vérification a eu lieu après le travail pertinent. [5]

J’exigerais quatre observations d’un banc de non-régression local : la requête a atteint le décodeur visé, le traitement s’est terminé, l’effet de bord interdit était absent, et une requête valide fonctionnait toujours. Un délai dépassé ne remplace pas un rejet. Un message d’exception ne remplace pas non plus l’observation des effets de bord : le chemin vulnérable peut lever une exception après l’exécution. Rattachez chaque assertion négative au chemin de code et à la condition de fin qu’elle prouve réellement.

Proposed regression evidence; not results from a run here

Affected build: path reached + completed + benign marker observed
Fixed build:    path reached + completed + marker absent
Valid input:   supported round trip still works
No completion: inconclusive, even when the marker is absent

Corriger les participants, puis vérifier qui peut les atteindre

Commencez la revue du déploiement par le listener. Par défaut, d’après la documentation, il est lié à localhost ; les déploiements multiprocessus qui l’exposent via --host ont une autre frontière, et les mainteneurs signalent leur ancien exemple Kubernetes. Localhost réduit l’exposition réseau mais n’authentifie pas un pair local. Établissez le mode, la version chargée, l’adresse d’écoute et les clients qui peuvent l’atteindre avant de prendre le score de criticité pour l’évaluation de votre déploiement. [1][6]

Mettez à jour ensemble les workers et les serveurs qui communiquent, car le format du descripteur a changé ; une flotte mixte n’est pas la stratégie de compatibilité prévue. La version 0.5.6 consigne la réparation, tandis que les mainteneurs décrivent séparément d’autres travaux de sécurité. Gardez le listener restreint après la mise à jour. La conclusion défendable est qu’un chemin de reconstruction précis a changé, pas qu’un service d’inférence est désormais sûr face à des clients arbitraires. [2][5]

Demandez à une revue de code de prouver la frontière

Une vulnérabilité utile doit nommer le point d’entrée, le hook personnalisé, l’opération dangereuse, ses préconditions et les contrôles qui interviennent trop tard. Sa vérification doit identifier la révision déployée et couvrir chaque appelant du décodeur partagé. C’est un livrable plus solide qu’un résultat de recherche indiquant qu’un projet contient pickle, car il dit à l’équipe quoi changer et comment savoir que la modification est arrivée en production.

C’est ce type de frontière que je suis lors d’une revue de code sécurité : le moment où des octets non fiables acquièrent un comportement, et l’hypothèse qui a fait paraître ce comportement nécessaire. Si vos services font transiter des objets par des caches, des files ou des utilitaires d’IPC, commencez par là. La partie difficile se trouve souvent plusieurs appels sous l’endpoint que tout le monde pensait avoir déjà examiné.

Sources

Besoin de vérifier ce point dans votre propre produit ?

Revue de code sécurité

Vous voulez apprendre à trouver vous-même ce type de vulnérabilité ?

Formation individuelle à la revue de code sécurisé
Tous les articles

08Contact

Vous livrez un produit qui traite des données d’utilisateurs ?

Envoyez-moi un bref descriptif. Je réponds sous trois jours avec des questions ou une première proposition, et le prix est fixé par écrit avant le démarrage.