You've probably run vLLM. But that KV-cache acceleration layer sitting next to it — the one that makes long prompts stop re-computing attention from scratch — you might not have checked which ports it opens.
On October 7, 2026, JFrog Security Research disclosed an unauthenticated remote code execution in LMCache's multiprocess mode: CVE-2026-105192, CVSS 3.1 score of 9.8. The mechanism isn't subtle. Its ZeroMQ transport (port 5555 by default) has no authentication, and the device IPC descriptors get deserialized with pickle.loads. A single crafted message executes arbitrary code with the privileges of the LMCache process — and in the official Docker image, that process is root. (source: JFrog JFSA-2026-001694382, LMCache Team)
Affected versions are 0.3.9 through 0.5.5, plus the 0.5.6 release candidates and pre-fix dev builds. The fix (swapping pickle for versioned MessagePack descriptors, PR #5644) merged October 12 and shipped in 0.5.6. (source: PyPI lmcache, GitHub GHSA-w5f2-9q9c-ff4h)
⚠️ The headline: if you're running LMCache in single-process (in-process) mode, the port isn't open by default, so you're largely fine. The risk is multi-node / distributed MP mode — the case where you explicitly bind the transport to a routable address so peers can connect. That's exactly the configuration the 9.8 score is measuring.
In plain terms
Imagine a datacenter where the machines share a "computation cache" (the KV cache — so a long prompt doesn't recompute attention every time) to speed each other up. To exchange those cache blocks quickly, they open an internal line — and because "these are all our own," the line has no doorbell.
pickle is where the door breaks. pickle is Python's "freeze an object into bytes, then restore it" format. It's convenient, but it executes data: restoring a pickle can call arbitrary functions. So a "cache block" that actually carries an object which runs os.system("...") means the machine, the moment it receives and unpacks it, has already run that code.
💡 The core: LMCache's transport binds to localhost by default (single-host safe), but the official Kubernetes example bound it to all interfaces, and the official images ran as root. If you deployed from that example, assume you're in the exposed group. (source: LMCache Team)
How the vuln actually works
LMCache's multiprocess (MP, a.k.a. distributed) mode opens a ZeroMQ ROUTER socket (port 5555 by default) so worker processes can register and share KV-cache blocks. The intended clients are sibling LMCache processes in the same cluster — but there's no CURVE, no ZAP, no password, no message auth. Nothing. (source: GitHub GHSA)
Messages on the socket are msgpack. Extension code 1 is registered for DeviceIPCWrapper, which flows into DeviceIPCWrapper.Deserialize in lmcache/v1/platform/base/ipc_wrapper.py, and that calls pickle.loads.
The timing is the nasty part: this hook fires while REGISTER_KV_CACHE arguments are still being decoded, before the handler even runs (it's the ext_hook in lmcache/v1/multiprocess/custom_types.py). In other words — your command has already run before the message is "properly processed." The handler may then log a type error because it expected a DeviceIPCWrapper and got the return value of os.system instead. That error is after the fact. (source: GitHub GHSA)
JFrog's PoC reduces to three steps:
- Start MP mode with the transport bound to a routable address:
python -m lmcache.v1.multiprocess.http_server \ --http-host 127.0.0.1 --http-port 8080 \ --host 0.0.0.0 --port 5555 ... - Build a
REGISTER_KV_CACHEframe whose cache slot is a pickle:class EvilPayload: def __reduce__(self): return (os.system, ("id > /tmp/zmq_pwn 2>&1",)) evil = pickle.dumps(EvilPayload()) # stuff it into msgpack ext code 1 - Send it over another ZMQ DEALER with
send_multipart. Done. On the official image (root),cat /tmp/zmq_pwnprintsuid=0(root). (source: GitHub GHSA)
Notice the __reduce__ returning (os.system, (cmd,)) directly — when pickle restores it, it's literally calling os.system(cmd) for you. This isn't a "parsing bug"; it's how the format is designed. pickle was always able to carry code.
Who's actually exposed
| Deployment | Exposed? |
|---|---|
| Single-process (in-process) mode | No — the port isn't open |
| MP mode, transport on localhost (default) | No — only that host can reach it |
MP mode, --host 0.0.0.0 / routable address |
Yes |
| Multi-node / distributed (peers connect) | Yes |
| Built from the old K8s example (bound all interfaces) | Yes (PR #5684 changed it back to localhost) |
That 9.8 is for the routable-configuration case. A stock single-host install that keeps the default bind isn't reachable from other machines — theoretically outside the exposed set. But that's not a guarantee you should lean on. (source: LMCache Team)
The fix: what 0.5.6 did
- Removed pickle: descriptors are now versioned, typed MessagePack, validated before use; the device-wrapper type is chosen from a fixed local allowlist, class names never come from network input, and there's no pickle fallback. Both the ZeroMQ and gRPC paths are covered, and malformed descriptors are rejected. (source: GitHub GHSA, LMCache Team)
- Official images now run as non-root (PR #5685)
- The K8s example now binds to localhost (PR #5684)
⚠️ Two tails 0.5.6 did not close (it only fixed the device-IPC path):
- RPC-transport auth — the RPC between the LMCache server and the MP coordinator / frontend still requires no auth; that's slated for 0.5.7. (source: LMCache Team)
- Pickle on the engine-driven path — CPU-data / retrieve-response handling still uses pickle, but that path is off by default and not network-reachable; they plan to remove it before 0.5.8.
The wire format changed: 0.5.6's descriptor format is not compatible with the old one. Upgrade all workers and servers together — you can't mix old and new. (source: GitHub GHSA)
Check whether you're hit
# 1. Your LMCache version
pip show lmcache | grep -E '^(Name|Version)'
# 0.3.9 <= v < 0.5.6 and running MP mode → in the affected range
# 2. Is the transport bound to a routable address? (the key check)
sudo ss -ltnp | grep ':5555'
# 0.0.0.0:5555 or 10.x / 192.168.x:5555 = exposed
# 127.0.0.1:5555 = only the local host reaches it
# 3. Any --host 0.0.0.0 in the launch args?
ps -eo args | grep -o 'lmcache[^\ ]*multiprocess[^\ ]* --host [^ ]*'
# 4. On K8s, what does the example you copied bind to?
grep -rn '0.0.0.0\|host:' <your lmcache deployment yaml>
Judging compromise (do what the LMCache team says literally): if the transport port was ever reachable from an untrusted network, treat the host as potentially compromised. Because the command runs before the message is processed, a rejected or malformed request in your logs does not mean the attack failed. (source: LMCache Team)
- Review connections to the transport port from unexpected sources
- Look for unexpected child processes, shells, or outbound connections from the LMCache process
- Rotate every credential / secret reachable from the affected containers, then redeploy from clean images
If You're Stuck
- Not sure if you're in MP mode → if you use
lmcacheas an in-process speedup to vLLM and never separately launchedmultiprocess.http_server, you're in-process, the port isn't open, and you're fine. If you ever ranpython -m lmcache.v1.multiprocess.http_serveryourself, that's MP mode. ss -ltnpdoesn't show :5555 but I'm uneasy → confirm whether LMCache is actually in MP mode. In-process mode doesn't open this port, so not seeing it is expected.- Multi-node and want to de-risk before upgrading → pull the transport back to localhost (or a trusted in-cluster net only), firewall port 5555 with a network policy, and drop the process to non-root (the 0.5.6 official images do this by default).
- After upgrading to 0.5.6, workers can't reach the server → the wire format changed. Upgrade every worker and the server at the same time; mixed versions aren't supported.
- I'm on 0.5.6 but want to confirm pickle is really gone → 0.5.6 fixed the device-IPC path. The engine-driven path (off by default, not network-reachable) sticks around until 0.5.8. Short term, nothing to do — but know it's still there.
- Want to confirm 0.5.6 is actually out →
pip install lmcache==0.5.6or just look at PyPI; the NVD record points at the v0.5.6 release too.
Where this fits in the series
This blog's security series has hit the same root cause over and over:
| Date | Topic | Shared trait |
|---|---|---|
| 11/1 | LMDeploy SSRF (CVE-2026-33626) | Inference server with no auth → used to reach in from inside |
| 11/3 | Langflow 33 CVEs | Agent builder's auto-login default → RCE without login |
| 11/4 | Node-RED (CVE-2026-58115) | IoT tool's no-auth default → one request, one shell |
| 11/6 | LMCache (CVE-2026-105192) | An inference accel layer's internal transport with no auth → one message, pickle RCE |
This one adds a layer: it's not the "facing-the-world API" — it's the internal line between machines. You usually don't treat internal lines as attack surface, but "defaults to friends" + "a format like pickle that executes data" = an attacker who just needs to be inside your trusted network has a direct route to root. (source: JFrog)
🔒 The general lesson: any "machines exchange data fast" internal port (ZMQ, gRPC, Redis, etcd, consul) should be protected like an external door — add auth, restrict the bind, put it behind a firewall. "Internal" doesn't mean "safe"; it means "the attacker has to spend one extra step to get in."
Further Reading
- Inference server SSRF: exploited in 12 hours — the inference server's "unauthenticated proxy" pattern, same "defaults to no-credential-to-the-world"
- Langflow's 33 CVEs, batched — the agent builder's auto-login default
- Node-RED, CVSS 10 — another one-HTTP-request-to-shell
- Two kernel CVEs in one day — the OS layer under your inference host
Sources
- LMCache Team: An Update on CVE-2026-105192 (2026-10-13, official: vuln, fix, exposure, compromise check, follow-up work)
- JFrog Security Research: JFSA-2026-001694382 — LMCache Unauthenticated RCE via Pickle Deserialization (CVSS 9.8, PoC, affected versions)
- GitHub Security Advisory GHSA-w5f2-9q9c-ff4h (affected
>=0.3.9 <0.5.6, fixed0.5.6, full PoC, workarounds, CVSS vector) - NVD: CVE-2026-105192 (2026-10-07, fixed version points to the v0.5.6 release)
- PyPI: lmcache (0.5.6 is live)
- Fix PRs (pickle → MessagePack), #5684 (K8s binds localhost), #5685 (non-root images)
