Pickle at the Door: Unauthenticated RCE in SGLang
SGLang serves large language models. Two of its features opened a network socket that ran whatever Python object you mailed it, with no login. That is unauthenticated remote code execution, and it is the same copy-paste mistake half the AI serving world shipped. This is CVE-2026-3059 and its two siblings.
CVE-2026-3059critical 9.8
Here is the whole thing in one breath. SGLang is a server that runs large language models. Two of its optional features open a network port, read whatever bytes arrive, and hand those bytes straight to Python’s pickle, which is allowed to build any object it is told to, including one whose construction runs a shell command. No password, no API key, no check of who is on the other end. Reach the port, mail an object, and the server runs your code as itself.
The part worth remembering is that this was not a clever one-off. The exact pattern, a ZeroMQ socket bound to the world plus pickle.loads() on the far side, had already been found and fixed in vLLM, in NVIDIA’s TensorRT-LLM, in Meta’s Llama Stack, and in Modular’s Max server. The code got copied around the AI serving ecosystem, and the bug rode along with it. SGLang was one of the last big frameworks still carrying it. These are CVE-2026-3059, CVE-2026-3060, and CVE-2026-3989.
Let me build it from zero.
What SGLang is and where it sits
A trained model is just a pile of numbers. Something has to load that pile onto a GPU and answer requests over an API. That something is an inference server. SGLang, from the SGL project, is one of the popular ones. It loads a model, keeps it hot on the GPU, and speaks the same HTTP API shape as OpenAI, so an application talks to it by POSTing to /v1/chat/completions.
Two of SGLang’s features matter here, and both are opt-in:
- Multimodal generation lets the server handle images and other media alongside text. To coordinate the extra work, it runs a small internal broker: a background task that receives jobs and passes them to the scheduler.
- Disaggregated serving splits one logical model across machines, so the “prefill” stage and the “decode” stage, or an encoder and a scheduler, can live on different boxes. To move data between those pieces, SGLang uses a fast messaging library called ZeroMQ.
Both features need parts of the system to talk to each other over the network. How they chose to talk is the bug.
Pickle and ZeroMQ, in plain terms
Two ordinary tools combine into one sharp edge.
Pickle is Python’s built-in way to turn a live object into bytes and back. It is convenient because it can serialize almost anything. It is dangerous because “back” is not a passive copy. A pickle stream is really a little program for a stack machine, and part of what it is allowed to say is “call this function with these arguments” while it rebuilds the object. If an attacker writes the stream, they choose the function. The Python documentation says it plainly at the top of the page: never unpickle data from an untrusted or unauthenticated source. pickle.loads(attacker_bytes) is, for practical purposes, “run the attacker’s code.”
ZeroMQ (ZMQ) is a messaging library, a layer over raw TCP sockets that makes request and reply patterns easy. A ZMQ socket can bind to an address and wait for messages. If it binds to tcp://*, the asterisk means every network interface on the machine, so anything that can route a packet to that port can send a message. There is no built-in authentication unless you deliberately add it.
Put them together, a socket bound to the world and a pickle.loads() on every message it receives, and you have built a remote code execution endpoint by accident.
The code in the middle
The multimodal broker lived in scheduler_client.py. Stripped to the essentials, it created a reply socket, bound it to every interface, then looped: receive bytes, unpickle them.
1
2
3
4
5
6
7
8
9
10
# python/sglang/multimodal_gen/runtime/scheduler_client.py (v0.5.9, simplified)
ctx = zmq.asyncio.Context()
socket = ctx.socket(zmq.REP)
broker_endpoint = f"tcp://*:{server_args.broker_port}" # * = all interfaces
socket.bind(broker_endpoint)
while True:
payload = await socket.recv()
request_batch = pickle.loads(payload) # untrusted bytes, straight in
...
Three details turn this from ugly into exploitable:
- It binds to the world.
tcp://*means the port answers to the whole network, not just to processes on the same machine. - There is no authentication. Nothing checks who sent the message. A
ServerArgsfield for the broker host existed but was ignored, so there was no supported way to lock it down either. - The port is guessable. By default the broker listens one above the HTTP port (
http_port + 1), so if the API is on the usual port, the broker is one door over.
The disaggregation path in encode_receiver.py had the same shape. Its socket helper defaulted to binding tcp://*, and the receiver did pickle.loads() on the incoming frame. That is CVE-2026-3060, reached with the --encoder-transfer-backend zmq_to_scheduler option.
The third issue, CVE-2026-3989, is quieter and local. A debugging helper, replay_request_dump.py, replays saved crash dumps with pickle.load(open(f, "rb")) and no validation. An operator who replays a crash dump handed to them, or one dropped into the dump folder, runs whatever is inside it. Lower reach, same root cause.
Why the exception handler does not save you
The broker loop wraps its work in a try/except. It does not help. The except only runs after pickle.loads() has already returned, and pickle.loads() is where the attacker’s code runs. By the time any error could be caught, the command has executed. Error handling that sits downstream of the dangerous call is decoration, not defense.
The three CVEs
All three share one root cause, CWE-502, deserialization of untrusted data.
| CVE | Where | CVSS | Reach |
|---|---|---|---|
| CVE-2026-3059 | Multimodal generation ZMQ broker (scheduler_client.py) | 9.8 Critical | Unauthenticated, network |
| CVE-2026-3060 | Disaggregation encoder receiver (encode_receiver.py) | 9.8 Critical | Unauthenticated, network |
| CVE-2026-3989 | Crash-dump replay helper (replay_request_dump.py) | 7.8 High | Local, needs a malicious .pkl |
The two network bugs need no credentials at all, which is why they score 9.8. The local one needs an operator to be tricked into replaying a poisoned dump, which is why it scores lower but is still ugly.
What an attacker gets
Code execution inside the SGLang process, running as whatever user and in whatever container that process runs as. On a model server, the neighborhood that opens up is exactly the valuable stuff: the model weights on disk, the data flowing through inference, any API credentials the process holds, the GPU, and the rest of the cluster the box can reach. From one unauthenticated message to a foothold on an AI serving node.
The important caveat, stated honestly, is that the two network bugs are only live when the matching feature is turned on. A default, text-only SGLang deployment does not start the multimodal broker or the disaggregation receiver, so it is not exposed to CVE-2026-3059 or CVE-2026-3060. The people at risk are the ones running multimodal generation or disaggregated serving, which is a growing slice as these deployments get bigger.
The pattern: this bug has a family
In November 2025, Oligo Security published research they called ShadowMQ, and it is the backdrop for this whole post. They traced the same mistake, an unauthenticated ZeroMQ socket feeding pickle.loads() or pyzmq’s recv_pyobj(), across a row of major AI serving projects. The code had been copied from one project to the next, and the vulnerability copied with it:
- Meta Llama Stack, CVE-2024-50050
- vLLM, CVE-2025-30165
- NVIDIA TensorRT-LLM, CVE-2025-23254
- Modular Max Server, CVE-2025-60455
By early 2026, most of those had been fixed, each in its own way: JSON instead of pickle, an HMAC check, a switch to msgpack. SGLang was one of the last still carrying the pattern on reachable paths, and that is what these CVEs closed. The lesson is not about one project. It is that “unpickle whatever arrives on this socket” became a house style across AI infrastructure, and the whole ecosystem is still paying it down. If you run any inference server, this is a class to grep your own deployment for, not a single product’s problem. The same shape shows up in the SSRF story next door in LMDeploy: an AI serving feature that trusts the network a little too much.
The fix, and an honest note about it
The fixes shipped in SGLang 0.5.10 (April 6, 2026). They came in two parts, and the split is worth understanding because it says something about how you actually kill this class of bug.
- Bind to localhost (PR #21435). For the two network CVEs, the broker and the encoder receiver now bind to
127.0.0.1by default instead oftcp://*. The operating system refuses remote connections before any byte reachespickle.loads(). A deployment that genuinely needs cross-machine traffic has to set the host on purpose. - Stop unpickling untrusted dumps (PR #20904). The crash-dump replay helper was moved to a
SafeUnpicklerthat refuses the known code-execution gadgets.
Notice what the network fix did and did not do. It closed the door by binding to loopback. It did not remove pickle from those paths. That is a reasonable first move, the kernel rejects the connection so the pickle never runs, but it means the safety now rests on the bind address staying correct. That assumption is exactly where the next bug came from: months later, the multimodal scheduler ingress was found still deriving its bind address from the public --host, so a server started on 0.0.0.0 re-exposed a pickle path. That was pinned to loopback in PR #36854 and shipped in 0.5.22 (October 2026). The durable fix for this whole class is to not deserialize untrusted bytes with pickle at all, and move to a format that cannot execute code, such as JSON or msgpack. Localhost binding buys safety; it does not end the conversation.
Who found what
Worth being precise, because the credit is shared. I discovered the two network bugs, CVE-2026-3059 and CVE-2026-3060, and wrote the proof-of-concept for each, as part of Orca Security’s research. The disclosure ran through CERT/CC as case VU#665416. Christopher Cullen of CERT/CC identified the third issue, CVE-2026-3989, the crash-dump replay path, and built the proposed patch. The SGLang maintainers did not respond during coordination, so CERT/CC published the advisory, and the community fixes landed in 0.5.10 shortly after.
Timeline
- Feb 4, 2026: Reported privately; GitHub Security Advisory opened and CERT/CC contacted.
- Feb 9, 2026: CERT/CC opens case VU#665416.
- Feb 23, 2026: CVE-2026-3059 and CVE-2026-3060 assigned.
- Mar 2 to 3, 2026: CERT/CC drafts a proposed patch (localhost binding plus msgpack).
- Mar 11, 2026: CVE-2026-3989 identified and assigned.
- Mar 12, 2026: Coordinated public disclosure, CERT/CC VU#665416.
- Mar 17, 2026: Covered by The Hacker News alongside the Bedrock and LangSmith findings.
- Mar 27, 2026: Community fix PRs merged.
- Apr 6, 2026: Fixes ship in SGLang 0.5.10.
- Oct 2026: A residual pickle path on the multimodal scheduler ingress is pinned to loopback in 0.5.22.
Details and credits
- Component: SGLang (serving framework for LLMs and multimodal models)
- Class: Deserialization of untrusted data (CWE-502)
- CVSS: 9.8 Critical for CVE-2026-3059 and CVE-2026-3060 (
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H); 7.8 High for CVE-2026-3989 (AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H) - Affected: SGLang <= 0.5.9 (network bugs require multimodal generation or disaggregated serving to be enabled)
- Fixed: 0.5.10 (PRs #21435 and #20904), with a follow-up hardening in 0.5.22 (PR #36854)
- Reported by: Igor Stepansky, Orca Security Research Pod (CVE-2026-3059, CVE-2026-3060); Christopher Cullen, CERT/CC (CVE-2026-3989)
- Coordination: CERT/CC, VU#665416
References
- Orca Security write-up: https://orca.security/resources/blog/sglang-llm-framework-rce-vulnerabilities/
- The Hacker News coverage: https://thehackernews.com/2026/03/ai-flaws-in-amazon-bedrock-langsmith.html
- GitHub advisory (CVE-2026-3059): https://github.com/sgl-project/sglang/security/advisories/GHSA-3cp7-c6q2-94xr
- CERT/CC VU#665416: https://kb.cert.org/vuls/id/665416
- NVD entry: https://nvd.nist.gov/vuln/detail/CVE-2026-3059
- Oligo Security, ShadowMQ: https://www.oligo.security/blog/shadowmq-how-code-reuse-spread-critical-vulnerabilities-across-the-ai-ecosystem
- CWE-502 (Deserialization of Untrusted Data): https://cwe.mitre.org/data/definitions/502.html