Skip to main content

One ZMQ Message to Root: What LMCache's CVE-2026-105192 Means for Self-Hosting the LLM KV-Cache Layer

9 min readDora NodaDora Noda
Share
On this page

A single unauthenticated network message could run arbitrary code as root on your inference cache server. That was the situation from October 7, when JFrog Security Research disclosed CVE-2026-105192 in LMCache's multiprocess mode, until this week, when LMCache shipped v0.5.6 with the pickle decode path removed. If you self-host LMCache in front of vLLM or SGLang, here is exactly what was exposed, how to tell whether it was you, and what to harden now.

The short version. The flaw (CVSS 9.8) affects LMCache 0.3.9 through 0.5.5 plus the 0.5.6 release candidates and the dev branch, and only deployments that bind the multiprocess transport to a routable address. Version 0.5.6, published October 14 with packages landing October 15, removes pickle from the device IPC descriptors and is the first fixed release. Your action list: upgrade to 0.5.6 or later, bind the transport to localhost unless peers genuinely need the network, scope the port with a NetworkPolicy so only your inference pods can reach it, and treat any routable pre-0.5.6 deployment as potentially compromised. One catch I verified while writing this: the v0.5.6 tag still ships the example Kubernetes manifest binding 0.0.0.0 — the localhost fix merged to the dev branch after the tag was cut. Details below.

The mechanism in 200 words​

LMCache is the open-source KV-cache layer that sits beside LLM inference engines, storing and reusing key-value caches across queries and engines so repeated context does not get recomputed (paper, docs). In multiprocess (MP) mode it runs as a standalone cache server, and worker processes register and share cache blocks with it over a ZeroMQ socket.

That socket has no authentication — no CURVE, no ZAP, no password, no message authentication — and one message type is decoded with Python's pickle.loads while the server is still reading the request's arguments, before any handler or type check runs. Concretely, a msgpack extension with code 1 reaches DeviceIPCWrapper.Deserialize in lmcache/v1/platform/base/ipc_wrapper.py, which unpickles it during REGISTER_KV_CACHE argument decoding. A single crafted ZMQ DEALER message to the transport port (default 5555) therefore executes code as the LMCache process user — and the official container images run that process as root, so JFrog's proof of concept ends with uid=0(root). The decode path shipped in v0.3.9 (October 2025) and was still present in 0.5.5, the 0.5.6 release candidates, and dev as of October 7. A public PoC has circulated since disclosure day.

Are you exposed?​

Exposure comes down to two questions: which version you run, and what address the transport binds. The default bind is localhost-only, and LMCache used inside a single vLLM process never opens the port at all. It becomes reachable only when an operator passes a routable --host, which is also what multi-node deployments do to share one cache across machines.

Deployment shapeExposed?
LMCache in-process (no MP server)No — the port is never opened
MP server on default localhost bindNo — unreachable from other machines
MP server with --host set to a routable address, version below 0.5.6Yes — unauthenticated RCE as the LMCache user
Deployed from the project's example lmcache-daemonset.yaml (v0.5.5 or the v0.5.6 tag)Yes — the example binds every interface (see next section)

The uncomfortable row is the last one. An operator who did the reasonable thing — starting from the project's own documented manifest — got the vulnerable configuration by default.

Don't trust the example YAML​

The Hacker News called this out on disclosure day: LMCache's own example Kubernetes deployment starts the server listening on every network interface. Here is the v0.5.5 manifest, still the first thing the MP deployment guide tells you to kubectl apply:

yaml
spec:
  hostNetwork: true   # pod shares the node's network namespace
  containers:
    - name: lmcache-server
      command:
        - /opt/venv/bin/python3
        - -m
        - lmcache.v1.multiprocess.server
        - --host
        - "0.0.0.0"   # every interface, on the host network
        - --port
        - "6555"

hostNetwork: true plus --host 0.0.0.0 means the unauthenticated ZMQ transport sits directly on the node's address. Note the port too: this example uses 6555, not the 5555 default, so scanners and firewall rules aimed only at the default would miss it.

The v0.5.6 release notes headline a localhost-bound example among their security hardening, crediting PR #5684. But I checked the tag itself: the v0.5.6 tag's daemonset still binds 0.0.0.0. The PR merged to the dev branch on October 13, after the release tag was cut, so the fix exists only on dev while the release notes describe it as shipped. The pickle removal itself (PR #5644) genuinely is in the tag — Deserialize there decodes a data-only descriptor and rejects legacy pickle — but the example hardening is not.

That split is the lesson in miniature. Vendored example YAML is a snapshot of someone else's judgment at tag time, and release notes describe intent while tags describe reality. If you started from this manifest, diff your copy against the tag you actually run — and assume any --host 0.0.0.0 you find there was a live RCE listener until you upgraded past it.

Eight days from disclosure to fix​

DateEvent
Oct 6One GitHub user files six additional LMCache security reports, alleging unauthenticated cross-tenant cache access and command-executing network services — unconfirmed, no CVE assigned (report index)
Oct 7JFrog (researcher Yuval Moravchick) publishes JFSA-2026-001694382 / CVE-2026-105192, CVSS 9.8, with a working PoC; The Hacker News coverage follows; no fixed version exists and LMCache has published no advisory
Oct 12–14v0.5.6 release assembled; published October 14 with security hardening as the headline: pickle removed from IPC descriptors, /run_script disabled by default with localhost-bound admin HTTP, fail-closed lookup locks, a SECURITY.md, and the project's first security advisory for the CVE
Oct 150.5.6 packages reach PyPI — the first installable fixed release

Eight days from public RCE to patch, with a public exploit in the wild the whole time, is a fast turnaround for a volunteer-maintained project. It is also eight days during which every routable MP deployment was one crafted message from compromise, with no vendor-provided way to detect whether the message had already arrived.

Harden it now: the checklist​

1. Upgrade to 0.5.6 or later. This is the fix — everything below is defense in depth around it:

bash
pip install -U "lmcache==0.5.6"

If you deploy the standalone container, move to the 0.5.6 image and pin to release tags rather than nightly, which is what the example manifest uses. Mixed-version fleets deserve a second look: a fixed server still speaks to workers, so confirm every MP-speaking component is at or above the fixed version before declaring the fleet clean.

2. Bind the transport to localhost unless peers need the network. Single-node deployments should keep the default localhost bind. If you copied the example manifest, change the --host argument to 127.0.0.1 (exactly what the pending upstream fix does) and point same-node vLLM pods at the loopback address. Only multi-node cache sharing genuinely needs a routable --host — and that is precisely the configuration the 9.8 score describes.

3. Scope the port with a NetworkPolicy. On Kubernetes, "trusted cluster network" should be a policy object, not a hope. This one lets only your vLLM pods reach the cache transport:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: lmcache-mp-ingress
  namespace: multi-process
spec:
  podSelector:
    matchLabels:
      app: lmcache-server
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: vllm-server  # adjust to your inference pods' label
      ports:
        - port: 6555            # or 5555 if you kept the default
          protocol: TCP

Heed JFrog's caveat when you write it: a firewall or policy lowers the risk by shrinking who can open a connection, but any host that can still reach the port on a vulnerable version can run code. Network controls are a complement to the upgrade, never a substitute for it.

4. Treat routable pre-0.5.6 deployments as potentially compromised. Neither JFrog's advisory nor the project's initial response gave operators a way to determine whether a server had already been attacked — no log signature, no indicator, no audit query. If your MP port was reachable on the network before you patched, do ordinary incident response: identify every exposed deployment, restrict access first, rotate credentials and tokens the LMCache process or its service account could read, and inspect surrounding systems for behavior consistent with code execution from that container or user account.

The AI data path needs a CVE watch​

The most transferable lesson is not about LMCache specifically. It is that the AI serving path — cache, router, gateway, connector — is now remotely reachable infrastructure with the same deserialization sins the rest of the industry spent a decade learning to avoid, and most teams do not watch it the way they watch their ingress or runtime CVEs.

This exact mistake keeps recurring. In November 2025, researchers grouped a set of pickle-on-ZMQ flaws across AI inference frameworks under the name ShadowMQ. Similar pickle-based RCE risks were reported against Meta's Llama Stack over insecure ZeroMQ. And LMCache's own ecosystem already had a adjacent warning: before vLLM 0.30.0 (September 22), a malformed cache_salt request could crash engines using the LMCache MP connector, a denial-of-service tracked as CVE-2026-105756 and rated 6.5. The cache connector was already a crash vector; it graduated to a code-execution vector two weeks later.

If you run GPU-backed inference on machines you own, put the whole serving path on the same footing as the rest of the fleet: subscribe to the security advisories of every component in it (LMCache now has one), pin versions you can reason about, default every new listener to localhost until a peer proves it needs the network, and back that default with a NetworkPolicy from day one. The next pickle-on-a-socket will not announce itself in your ingress dashboard. It will sit one layer deeper, in the component you deployed from an example and never audited.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. If you are self-hosting GPU-backed inference next to your web services, star the repo on GitHub and keep the whole serving path — cache included — behind infrastructure you control.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex