SM

Command Palette

Search for a command to run...

Blog

One Flag Away From Root: What I Learned Researching the LMCache Pickle RCE

Syed Moinuddin13 min read
SecurityAI EngineeringLLMsBackendDev Tools
One Flag Away From Root: What I Learned Researching the LMCache Pickle RCE

CVE-2026-105192 lets one unauthenticated ZeroMQ message run code on an LMCache multiprocess server, and no fixed version exists. The default bind is safe; LMCache's own Kubernetes example is not. Who can reach the port, why this bug keeps recurring, and four checks to run today.

Here is the whole argument up front. CVE-2026-105192 lets anyone who can open a TCP connection to an LMCache multiprocess server run code on it with a single message, as root on the official images, and there is no patched version to install S1S2. The bug is not exotic. LMCache hands bytes from an unauthenticated ZeroMQ socket to Python's pickle, the same mistake that has now shipped in eight LLM serving projects since 2024 S3S4S5. So for the next few weeks the fix is not a version number, it is one flag. With the default --host localhost, nothing off the machine can reach the port. With --host 0.0.0.0, which is what LMCache's own Kubernetes example uses next to hostNetwork: true, anything that can reach the node can S1S6.

I started this as a quick "upgrade now" note, like my Next.js security post. Then PyPI showed there is nothing to upgrade to: twenty stable releases carry the bug, and the release candidate published the day before disclosure still has it S7S1. What changed the post was the example DaemonSet, which puts the port on the node IP, where most network plugins do not apply pod policy selectors S6S8. "My reading" below means the 0.5.5 and 0.5.6rc3 wheels I pulled from PyPI on October 8. I read the code. I did not run the exploit.

Table of four ways to run LMCache: in-process inside vLLM opens no port; --host localhost on port 5555 is reachable only from the same machine; --host 0.0.0.0 on the pod network is reachable from anything routable to the pod IP; the example DaemonSet with hostNetwork and 0.0.0.0 on port 6555 is reachable from anything that can reach the node IP, where pod selectors usually do not apply.

FIG. 01 · One flag decides who can reach the cache server · the default is local, the example DaemonSet is not S1S6S8

What does one message actually do?

LMCache is a KV cache layer for vLLM and other engines. In multiprocess mode it runs as a separate server, one per node, that vLLM workers reach over ZeroMQ or gRPC S9. The ZeroMQ side is a ROUTER socket with no CURVE, ZAP, password or message authentication S1. One msgpack extension type, code 1, carries a DeviceIPCWrapper, the handle to GPU memory a worker wants to share, and the server rebuilds it with pickle.loads so the concrete subclass survives the trip S1S10.

The order is the problem. The pickle runs inside ext_hook while the server is still decoding a REGISTER_KV_CACHE message, before any handler looks at what arrived S1. In JFrog's proof of concept, one DEALER message to port 5555 makes the server write uid=0(root) to a file, and only then does it log a type error S1. Yuval Moravchick of JFrog found the bug, and JFrog scores it 9.8 for a server bound to a routable address S1.

Six-step flow: a DEALER message reaches the ZeroMQ ROUTER on port 5555 with no authentication, msgpack decodes the message arguments, ext_hook code 1 hands the bytes to DeviceIPCWrapper, pickle.loads runs the attacker's code, and only then does the handler's type check log an error. Affected 0.3.9 to 0.5.5 and 0.5.6rc1 to rc3, with no fixed release as of October 7, 2026.

FIG. 02 · The pickle runs before the server knows what it received · the failed type check comes after the command S1S10

Who can actually reach the port?

This is the part worth an afternoon. JFrog is careful about scope: the request server binds localhost unless the operator passes a routable --host, a stock single-host install is not reachable from other machines, and LMCache inside a vLLM process opens no port at all S1. My reading of both wheels agrees: --host defaults to localhost and --port to 5555.

The example people copy does not use the default. LMCache's v0.5.5 lmcache-daemonset.yaml starts the server with --host "0.0.0.0" and --port "6555", sets hostNetwork: true, and pulls lmcache/standalone:nightly S6. Host networking puts the socket on the node's own interfaces, so the question becomes who can reach the node IP on 6555. The usual Kubernetes answer, a NetworkPolicy, is the wrong tool. The docs call policy behavior for hostNetwork pods undefined, and say the most common implementation ignores those pods when matching selectors and treats their traffic as node traffic S8. An ipBlock rule against node IPs can still work. A policy that selects app: lmcache-server probably does nothing. Note the port, too: a scanner keyed to JFrog's 5555 walks past a server started from the example on 6555 S1S6.

Is there a fixed version yet?

No. JFrog lists 0.3.9 through 0.5.5, the 0.5.6 release candidates through rc3, and the dev branch as of October 7, with no fixed release S1. The PyPI dates make the window concrete: 0.3.9 shipped on October 29, 2025 and 0.5.5 on September 12, 2026, which is twenty stable releases, not counting four post releases, in the 343 days before disclosure S7. LMCache had not published a security advisory when The Hacker News checked S2, and PyPI still listed 0.5.6rc3 as the newest release when I checked on October 8 S7.

Timeline of 20 stable LMCache releases from 0.3.9 on October 29, 2025 to 0.5.5 on September 12, 2026, all affected, followed by 0.5.6rc1 to rc3 and disclosure of CVE-2026-105192 on October 7, 2026: 343 days from the first affected release, 25 days from 0.5.5, and zero fixed versions.

FIG. 03 · Twenty stable releases in 343 days, none fixed · hollow dots are the 0.5.6 release candidates, still affected S7S1

I compared the two wheels to see what the release candidate does change. One thing got safer: the default --http-host of the multiprocess HTTP server moved from 0.0.0.0 in 0.5.5 to 127.0.0.1 in 0.5.6rc3, which matches what The Hacker News reported S2. That matters because a separate report says the same HTTP server on 0.5.5 will clear the cache, rewrite tenant quotas and return the whole process environment from GET /env, all without authentication S11. That report and five others came from one GitHub account on October 6, with proofs of concept but no maintainer reply or CVE, so I treat them as claims S2S11S12.

One thing did not change, and it is wider than the advisory. Both wheels contain a second server-side pickle.loads in the engine-driven transfer code, on the bytes a worker sends with COMMIT_STORE. LMCache's docs describe that route as the "pickle path" S9. It only loads when --supported-transfer-mode is engine_driven or auto, JFrog does not mention it, and I did not test whether it is reachable without first registering a context. I am flagging it for one reason: when a fix ships, check that pickle left the transport, not only DeviceIPCWrapper.

Comparison of LMCache 0.5.5 and 0.5.6rc3 wheels: the HTTP server default bind changed from 0.0.0.0 to 127.0.0.1, the request server still binds localhost on port 5555, DeviceIPCWrapper.Deserialize still calls pickle.loads, and the engine-driven commit_store path also still calls pickle.loads, which the advisory does not mention.

FIG. 04 · What 0.5.6rc3 changed, and what it did not · my reading of the PyPI wheels, not a test run S7S1S9

I also looked for practitioner discussion and found none on Hacker News in the first day, so the record so far is a security vendor, a news write-up and one GitHub reporter. The most useful dissent is JFrog's own scoping: on a default single-host install, this is not a remote bug at all S1.

Why does this keep happening in LLM serving?

Because the code travels. Oligo's ShadowMQ research in November 2025 traced pickle over unauthenticated ZeroMQ TCP sockets through Meta's Llama Stack, vLLM, NVIDIA TensorRT-LLM, Modular Max Server, Microsoft's Sarathi-Serve and SGLang, and found that SGLang's vulnerable file opened with a note saying it was adapted from vLLM S3. Oligo's Avi Lumelsky summed it up as different maintainers at different companies making the same mistake S3. Since then SGLang picked up more pickle CVEs in March 2026, addressed in 0.5.10 S4, and LMDeploy's disaggregated serving path got CVE-2025-66455, fixed in 0.16.0 by moving that channel from pickle to JSON S5. LMCache is the eighth.

Timeline from 2024 to 2026 of pickle over unauthenticated ZeroMQ flaws in eight serving projects: Meta Llama Stack CVE-2024-50050 fixed with JSON, vLLM CVE-2025-30165 fixed with a new default engine, NVIDIA TensorRT-LLM CVE-2025-23254 fixed with HMAC, Modular Max Server CVE-2025-60455 fixed with msgpack, Microsoft Sarathi-Serve unpatched, SGLang CVE-2026-3059 and 3060 fixed in 0.5.10, LMDeploy CVE-2025-66455 fixed in 0.16.0 with JSON, and LMCache CVE-2026-105192 with no fix.

FIG. 05 · Eight serving projects, one mistake, two years · hollow dots had no fix when reported S3S4S5S1

The library warned everyone. PyZMQ's docs say recv_pyobj is a thin wrapper over pickle.loads that trusts incoming messages with arbitrary code execution, and point to CURVE, IPC socket permissions or an HMAC checked before deserializing S13. LMCache never calls recv_pyobj. It reached pickle through a msgpack extension hook, which is exactly why a grep for recv_pyobj misses it. The projects that fixed this picked JSON, msgpack, HMAC validation or, for vLLM, a new default engine S3S5. vLLM's security guide is the honest summary for operators: every inter-node channel, KV cache transfer included, is insecure by default and belongs on an isolated network S14.

How would I check mine this afternoon?

Four checks, in this order. They assume Linux nodes and kubectl access, and they only read state. I tested the jq filter against sample pod JSON, not against your cluster.

# 1. Is multiprocess mode running anywhere? Read pod specs, not Helm values.
kubectl get pods -A -o json | jq -r '.items[]
  | select([.spec.containers[] | ((.command // []) + (.args // [])) | join(" ")]
           | any(test("lmcache(\\.v1\\.multiprocess| server)")))
  | "\(.metadata.namespace)/\(.metadata.name) hostNetwork=\(.spec.hostNetwork // false)"'
 
# 2. On each node that runs one: what is actually bound?
#    Anything other than 127.0.0.1 or ::1 is reachable off-host.
sudo ss -ltnp | grep -E ':(5555|6555|8080)\b'
 
# 3. From a different node: does the port accept a connection?
#    Connecting proves exposure. Do not send anything.
nc -vz -w 3 NODE_IP 6555
 
# 4. Who would injected code run as?
kubectl exec -n NAMESPACE POD -- id

If check 2 shows a routable bind, the fix depends on where your workers live. If they share the host's network, bind to 127.0.0.1 S1. If they reach the server through the node IP, as the DaemonSet layout implies, filter the port at the host. This is a sketch I have not run against a real CNI, and eBPF-based plugins may bypass iptables entirely:

# On each LMCache node: allow 6555 only from loopback and this node's pod range.
# .spec.podCIDR is empty on plugins that manage their own IP ranges; use your pod subnets then.
POD_CIDR=$(kubectl get node "$(hostname)" -o jsonpath='{.spec.podCIDR}')
sudo iptables -I INPUT -p tcp --dport 6555 -j DROP
sudo iptables -I INPUT -p tcp --dport 6555 -s "$POD_CIDR" -j ACCEPT
sudo iptables -I INPUT -p tcp --dport 6555 -s 127.0.0.1 -j ACCEPT

Keep JFrog's caveat in mind: anything that can still connect can still run code S1. On a node that also runs workloads you do not trust, turn multiprocess mode off until a release takes pickle out of the transport.

Four read-only checks in order: whether LMCache multiprocess mode is running, checked with kubectl; what it binds to on ports 5555, 6555 or 8080, checked with ss; who can reach it, checked with nc from another node; and which user it runs as, checked with id in the container. Until a fixed release ships, the bind address and the host firewall are the patch.

FIG. 06 · Four read-only checks, in order · the bind address and the host firewall are the patch for now S1S6S8

What does the side-by-side look like?

Deployment shapeWho can reach the request portDoes a NetworkPolicy help?What I would do now
LMCache inside the vLLM processNo port is opened S1Not neededNothing for this CVE
lmcache server with the default --host localhostProcesses on the same machine S1Not neededKeep the default and check nobody overrides it
--host 0.0.0.0 on the pod networkAnything that can route to the pod IPYes, if your plugin enforces policy S8Bind narrower and add an ingress policy
The example DaemonSet, hostNetwork: true on 0.0.0.0:6555Anything that can reach the node IP S6Usually not, selectors skip host-network pods S8Filter at the host, or turn multiprocess mode off

What should you do this week?

If you run LMCache multiprocess mode:

  • Read --host and --port from every running server, and treat any non-loopback bind as exposed S1
  • Check ports 6555 and 8080 as well as 5555 S6S11
  • Upgrade vLLM to 0.30.0 or later, which fixes a one-request engine crash on LMCache multiprocess deployments S15
  • Watch the LMCache security advisories page for the fix S2

If you copied the example DaemonSet:

  • Do not count on a NetworkPolicy that selects the pod S8
  • Filter the port on the node, or move the server off host networking S6
  • Confirm which user the container runs as S1

If you only use LMCache inside vLLM:

  • Nothing to patch for this CVE, but run check 1 to confirm no multiprocess server exists S1

If you maintain anything that speaks ZeroMQ:

  • Grep for pickle.loads and recv_pyobj anywhere socket bytes flow, including codec hooks S13
  • Authenticate before you deserialize: CURVE on the socket or an HMAC on each message S13S1

What would I do differently next time?

  1. Read the example deployment before the severity score. The 9.8 is conditional on a routable bind, and the example YAML is what tells you whether your cluster meets the condition.
  2. Search for the class, not the CVE. Grepping a dependency tree for pickle.loads near socket code finds this bug and the next one, without waiting for a feed to catch up.
  3. Treat "localhost by default" as a property of the binary, not of the deployment. Every exposed server in this story is a safe default plus one flag that someone added for a good reason.

FAQ

  1. What is CVE-2026-105192?

    An unauthenticated remote code execution flaw in LMCache multiprocess mode, disclosed by JFrog on October 7, 2026. One ZeroMQ message carrying a Python pickle runs code as the LMCache process, which JFrog says is root on official images. JFrog scores it 9.8 for servers bound to a routable address.

  2. Which LMCache versions are affected?

    0.3.9 through 0.5.5, the 0.5.6 release candidates through rc3, and the dev branch as of October 7, 2026. No fixed version had been released on that date.

  3. Am I affected if I only use LMCache inside vLLM?

    Not by this CVE. LMCache running inside a vLLM process does not open the multiprocess port. Only the standalone multiprocess server, started with lmcache server or lmcache.v1.multiprocess, opens it.

  4. Is the default configuration exposed to other machines?

    No. The request server binds localhost on port 5555 by default. It becomes reachable from other hosts when it is started with a routable --host such as 0.0.0.0.

  5. Why does the LMCache example DaemonSet matter?

    The v0.5.5 example starts the server with --host 0.0.0.0 on port 6555 and sets hostNetwork: true, which puts the port on the node IP. Kubernetes says most network plugins ignore host-network pods when matching NetworkPolicy selectors.

  6. Will a NetworkPolicy protect a hostNetwork LMCache pod?

    Usually not if the policy selects the pod by label. Kubernetes calls the behavior undefined and says the most common implementation treats that traffic as node traffic. An ipBlock rule or a host firewall is more reliable.

  7. What should I do until a fix ships?

    Bind the multiprocess server to 127.0.0.1 if its workers share the host network. Otherwise restrict the port at the host to the node's own pods, and turn multiprocess mode off on nodes that run untrusted workloads.

  8. Does LMCache 0.5.6rc3 fix anything?

    It changes the default bind of the multiprocess HTTP server from 0.0.0.0 to 127.0.0.1. The pickle.loads call behind CVE-2026-105192 is still present, according to JFrog and a reading of the wheel.

  9. Is this the same bug as ShadowMQ?

    Same class, different code. ShadowMQ is Oligo's 2025 name for pickle over unauthenticated ZeroMQ sockets in Llama Stack, vLLM, TensorRT-LLM, Modular Max Server, Sarathi-Serve and SGLang. Whether LMCache shares code with those projects has not been established.

  10. Which ports should I check?

    5555 is the request server default, the example DaemonSet uses 6555, and the multiprocess HTTP server listens on 8080.

Sources

  1. S1JFrog Security Research, "LMCache is vulnerable to Unauthenticated Remote Code Execution via Pickle Deserialization on the Multiprocess ZMQ Transport, JFSA-2026-001694382," October 7, 2026, research.jfrog.com
  2. S2The Hacker News, "Unpatched Critical LMCache Flaw Lets Unauthenticated Attackers Run Code Remotely," October 7, 2026, thehackernews.com
  3. S3Oligo Security, Avi Lumelsky, "ShadowMQ: How Code Reuse Spread Critical Vulnerabilities Across the AI Ecosystem," November 13, 2025, oligo.security
  4. S4CERT/CC, "SGLang (sglang) is vulnerable to code execution attacks via unsafe pickle deserialization, VU#665416," March 12, 2026, kb.cert.org
  5. S5OSV, "CVE-2025-66455: LMDeploy has Remote Code Execution by Pickle Deserialization via handle_zmq_recv," September 18, 2026, osv.dev
  6. S6LMCache, "examples/multi_process/lmcache-daemonset.yaml at v0.5.5," accessed October 8, 2026, github.com
  7. S7PyPI, "lmcache release history," accessed October 8, 2026, pypi.org
  8. S8Kubernetes, "Network Policies," updated April 14, 2026, kubernetes.io
  9. S9LMCache, "Overview, multiprocess mode," accessed October 8, 2026, docs.lmcache.ai
  10. S10LMCache, "lmcache/v1/platform/base/ipc_wrapper.py at v0.5.5," accessed October 8, 2026, github.com
  11. S11yyymk, "Unauthenticated cache control plane and process environment disclosure on LMCache multiprocess HTTP server," GitHub issue 5511, October 6, 2026, github.com
  12. S12yyymk, "Unauthenticated cross-tenant KV cache read, write, and unbounded storage on LMCache server," GitHub issue 5507, October 6, 2026, github.com
  13. S13PyZMQ, "Serializing messages with PyZMQ," PyZMQ 27.2.0 documentation, accessed October 8, 2026, pyzmq.readthedocs.io
  14. S14vLLM, "Security," updated October 6, 2026, docs.vllm.ai
  15. S15vLLM, "Loose cache_salt validation lets a single request kill EngineCore on LMCache-MP deployments, GHSA-2823-qmq8-rwvj," September 23, 2026, github.com

Written by

Syed Moinuddin

Full Stack Engineer.

Notes on AI tooling, agentic systems, and building things that survive contact with production.

Command Palette

Search for a command to run...