← Notes

CVE-2026-49950: Pre-Auth RCE in the Xinference/Xoscar Actor Transport

Overview

CVECVE-2026-49950
Xinference advisoryGHSA-2hj6-h88x-mv4j
Xoscar advisoryGHSA-mmvj-m246-m9vf
ComponentsXinference actor transport; Xoscar Socket and UCX receive paths
AffectedXinference ≤ 4.0.1 and Xoscar 0.1.0–0.12.0 when actor authentication is disabled and an actor port is reachable by an untrusted peer
Version-only fixNone; Xinference 4.0.1 with Xoscar ≥ 0.12.0 must also use a strong, nonempty XOSCAR_AUTH_KEY on every actor peer
ClassCWE-306 (Missing Authentication for Critical Function), CWE-502 (Deserialization of Untrusted Data)
CVSS v4.09.3 (Critical) — CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Reporter creditLukas Johannes Moeller (@JohannesLks); the vendor advisory also credits independent reporters

TL;DR

Xinference protects its HTTP API, but its internal Xoscar actor transport was a separate unauthenticated network service. Any peer that could reach an actor-pool port could send a serialized Python object. Xoscar called cloudpickle.loads() before actor dispatch, so the object's reduction logic executed with the actor process's privileges.

The bug is not "pickle exists." Xoscar legitimately needs Python-object transport between trusted cluster processes. The bug is that network reachability was the only thing separating an untrusted machine from that deserializer.

Root Cause

The vulnerable receive path in Xoscar 0.11.2 was direct:

# python/xoscar/serialization/aio.py
async def _get_obj(self):
    header = cloudpickle.loads(await self._get_obj_header_bytes())
    buffer_sizes = header[0].pop(BUFFER_SIZES_NAME)
    buffers = [await self._readexactly(size) for size in buffer_sizes]
    return deserialize(header, buffers)

There was no peer-authentication check before the first cloudpickle.loads(). The header itself was already executable Python serialization; additional payload buffers then entered Xoscar's general deserializer.

The transport also accepted normal actor control messages from unauthenticated peers. The upstream advisory documents get_config exposing pool topology, module configuration, and subprocess environment dictionaries, while stop shuts down an actor pool. Those operations need no malicious pickle object. They are a second consequence of the same missing trust boundary.

Attack Path

A reducer is enough to turn deserialization into command execution:

class Payload:
    def __reduce__(self):
        import os
        return (os.system, ("id > /tmp/xoscar-rce",))

The network path is:

untrusted peer
  -> reachable actor-pool socket
  -> Xoscar frame header
  -> cloudpickle.loads(header)
  -> Payload.__reduce__()
  -> command runs as the actor-pool process

Actor-level routing is too late to stop this. Python code has already run by the time Xoscar can decide which actor or method should receive the message.

My original report established this path through source review. It did not pretend that a guessed four-byte length prefix was a working wire-level exploit. The final vendor advisories cover the complete Socket and UCX receive paths and the maintainers' validation of the control channel.

Reachability

Distributed Xinference deployments use network actor ports independently of the HTTP API. The supplied distributed Compose topology publishes ports 9999, 30001, and 30002; an untrusted party able to reach one of those unauthenticated listeners can target the actor process directly.

The issue is not limited to distributed mode as a code property. A single-node deployment is also exposed when its actor listener is reachable by an untrusted peer. A loopback-only listener reduces the reachable population to local processes, but it does not authenticate them.

Xinference's REST authentication does not cover these ports. An API key and the actor-cluster secret belong to different trust domains: protecting port 9997 does not protect a reachable actor listener. Likewise, hiding /v1/address does not fix an exposed transport.

Impact

  • Pre-auth Python code execution as the receiving supervisor, worker, or actor-pool process.
  • Access to model data, inference inputs and outputs, credentials, mounted secrets, and files visible to that process.
  • Cluster manipulation and denial of service through direct actor operations.
  • A compromised supervisor can become a foothold toward connected workers; host impact depends on container isolation, mounts, and process privileges.

Fix

Xoscar 0.12.0 added opt-in HMAC-SHA256 authentication for Socket and UCX actor messages. In authenticated mode it verifies framing metadata, the serialized header, and every payload buffer before reconstructing Python objects. Messages with a missing or incorrect key, or an invalid signature, are rejected before deserialization and dispatch.

The ordering is the fix. Xoscar authenticates a fixed-width manifest, reads the declared header and buffers, verifies the complete frame, and only then calls cloudpickle.loads():

if not verify_buffers(auth_key, (prefix, manifest, header_bytes, *buffers), frame_mac):
    raise EOFError("Frame payload authentication failed")

return cloudpickle.loads(header_bytes)

Xinference 4.0.1 requires Xoscar 0.12.0 or later and passes the actor key through its standalone and distributed Compose configurations. The integration landed in PR #5696.

There is no version-only fix. Authentication stays disabled when XOSCAR_AUTH_KEY is unset or empty, so upgrading by itself leaves the legacy transport exposed. Operators must generate one deployment secret and provide the same value to every supervisor, worker, sub-pool, and direct actor client:

python -c "import secrets; print(secrets.token_hex(32))"
export XOSCAR_AUTH_KEY='<same-random-deployment-secret>'

The key belongs in the deployment's secret-management system, not in source control or a container image. Model processes inherit it from their parent. All communicating processes must be restarted together: keyed and unkeyed peers cannot interoperate, and there is no mixed-mode rolling upgrade.

Network controls remain part of the fix. Actor ports should be reachable only by intended cluster peers, even after message authentication is enabled.

Limits

The HMAC restores peer authentication, not fine-grained authorization or a complete encrypted channel:

  • Anyone holding the shared key is a trusted actor peer with Python execution privileges in receiving processes; there is no per-user or per-actor authorization layer.
  • HMAC authentication detects unauthenticated or modified messages but does not encrypt traffic.
  • It does not prevent replay of captured messages. Deployments that need confidentiality or replay protection should place the actor transport inside an authenticated, encrypted tunnel.
  • Authenticated GPU transfers, including UCX, stage device buffers in host memory for hashing and add CPU and host-memory overhead.

If coordinated key rollout is not immediately possible, remove all untrusted reachability to actor-pool ports with host firewalls, private networks, or container-network policy. Every host that can reach an unauthenticated actor port must be treated as able to execute Python in the receiving process.

Disclosure

I found the unauthenticated actor-transport deserialization path during a source audit in March 2026 and reported it privately to the Xinference maintainers. Xorbits published the Xinference and Xoscar advisories on October 16, 2026 alongside Xoscar 0.12.0 and Xinference 4.0.1. The final advisory credits me and multiple independent reporters.