Pickle over gRPC: unauthenticated RCE in a robot policy server
Hugging Face's LeRobot ran its robot-to-policy inference channel on pickle over unauthenticated gRPC — any host that reached the port got remote code execution. The June 2026 fix drops pickle entirely.
What is this?
On June 29, 2026, SecureLayer7’s research team published a write-up of an unauthenticated remote-code-execution flaw in LeRobot, Hugging Face’s open-source robotics framework. LeRobot’s async inference pipeline splits a robot policy into a client–server pair: a client running on the physical robot streams observations to a PolicyServer on a separate GPU host, which streams actions back. The transport is gRPC — and the payloads were carried as raw Python pickle bytes.
The flaw is the predictable consequence of that choice. This write-up is based on the public advisory and the upstream fix; no working exploit is reproduced here. The lesson is deployment-shaped rather than model-shaped: when a serving component deserialises untrusted network data with pickle, and the channel has no authentication, the network is the vulnerability.
How it works
Python’s pickle is not a data format — it is a small stack-machine bytecode with opcodes that invoke arbitrary callables during load. CPython says so plainly: “The pickle module is not secure. Only unpickle data you trust.” Any object can define what happens on unpickling, so a crafted blob can run a command the moment it is deserialised, before any downstream validation runs.
In the affected release, the async-inference handlers read attacker-reachable bytes straight off the wire and passed them to pickle.loads():
# vulnerable pattern (illustrative)
policy_specs = pickle.loads(request.data) # nosec <-- runs before validation
Two problems compounded. First, the deserialisation happened before the type check meant to validate the message, so the object was already constructed — and construction is where the dangerous work occurs. Second, the gRPC listener was bound with add_insecure_port(...): no TLS, no token, no mutual auth. The default topology assumes the robot and the policy server share a trusted network (a VPC or a Tailscale mesh), but the moment the port (TCP/50051 by default) is reachable by any untrusted actor, that assumption fails open. The bug is also bidirectional: the server pickled its action responses too, so a malicious or compromised policy server could turn the same primitive against every robot client that connected to it.
A recurring smell in the code was a # nosec comment on each pickle call — an annotation that silences the static-analysis warning (Bandit’s B301) without addressing the runtime risk it flags.
Why it matters
The affected code shipped in the framework used to train and serve real-robot policies, so the exposed population includes shared research GPU clusters, multi-machine training topologies, demo notebooks left running, and cloud-hosted deployments where the operator never realised gRPC was listening. A single reachable port yields code execution as the server process — reported as root inside the reference container — which on a GPU host usually means model artifacts, datasets, and cloud credentials sit one step away.
There is a pointed irony here: Hugging Face authored safetensors specifically to kill “the pickle problem” for model files, and its documentation says in bold not to load pickle files from untrusted sources. The same anti-pattern reappeared in a different corner of the stack — the inference transport — which is exactly where deserialisation bugs keep hiding across the AI serving ecosystem.
Defenses
Never pickle.loads() network-controlled data. The durable fix is a typed, non-executable wire format. The upstream patch replaced pickle with safetensors for tensors and JSON for metadata, wrapped in an envelope that carries a type tag the deserialiser validates against the expected type. That removes the opcode interpreter, enforces a schema, and rejects a message routed to the wrong handler.
Authenticate the transport. gRPC does not imply authentication — mTLS or token auth is opt-in. Bind serving endpoints with credentials, not add_insecure_port, and require mutual TLS between robot and policy server.
Treat the network as hostile. Default-deny inbound to inference ports, bind to loopback or a private interface, and never expose a policy/inference port to a network any untrusted party can reach. Segment GPU hosts away from general workloads.
Hunt the pattern statically. Grep network-facing Python services for pickle.loads on anything tracing back to request input — and treat a # nosec sitting next to it as a finding, not a resolution.
Least privilege and egress control. Scope the serving process’s cloud role to the minimum, and alert on a policy-server process spawning an unexpected child (a shell, an interpreter) or making outbound connections it should never make.
Status
| Item | Detail |
|---|---|
| Reference | CVE-2026-25874 (see cve.org record) |
| Disclosed | Advisory published June 29, 2026 |
| Affected | LeRobot async-inference (PolicyServer / robot client) in v0.4.3 and earlier |
| Root cause | pickle.loads() on unauthenticated gRPC input, deserialised before type validation |
| Impact | Pre-auth remote code execution on the policy server; bidirectional (malicious server can hit clients) |
| Fix | Upstream PR #3048 — pickle removed, replaced with safetensors + JSON typed schema; upgrade and enable transport auth/network isolation |