The Socket Is the Authorisation: Xoscar’s Actor Pool Unpickles Whatever Connects, and Xinference Ships It

CVE-2026-49950 reached NVD on 13 October 2026 at 21:17 UTC, assigned by VulnCheck, scored CVSS 4.0 9.3 (AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H), CWE-502, status Received. The affected product is Xorbits Xoscar, a Python actor framework that almost nobody deploys deliberately — and that a great many people deploy anyway, because it is the transport layer underneath Xinference, the 9,600-star model-serving stack from the same authors.

The record says “through 0.11.2,” and 0.11.2 is the current release, tagged 7 October 2026 and uploaded to PyPI the same morning. There have been no commits to the repository since. There is no GitHub Security Advisory on the project. As of this writing there is no fixed version to upgrade to.

What the code does

We read the two files the CVE references, at tag v0.11.2. The receive path in python/xoscar/backends/communication/socket.py is six lines:

@implements(Channel.recv)
async def recv(self):
    deserializer = AioDeserializer(self.reader)
    async with self._recv_lock:
        header = await deserializer.get_header()
        buffers = await read_buffers(header, self.reader)
    return deserialize(header, buffers)

Follow deserialize into python/xoscar/serialization/core.pyx and the terminus is unpickle_buffers:

result = cloudpickle.loads(buffers[0], buffers=buffers[1:])

There is no step between those two excerpts that authenticates the peer, and none that inspects the message before reconstructing it. That ordering is the whole vulnerability. A pickle stream does not describe an object; it describes a short program that builds one, and __reduce__ gadgets execute during loads(). By the time recv() returns a value for anyone to type-check, the attacker’s code has already run. Validation downstream of deserialisation validates something that was decided earlier.

The listener is equally plain. In the same file, handle_connection hands every accepted socket straight to the channel:

async def handle_connection(reader: StreamReader, writer: StreamWriter):
    # create a channel when client connected
    return await server.on_connected(
        reader, writer, local_address=server.address
    )

and on_connected itself does nothing with credentials — it pops local_address and dest_address from its keyword arguments, raises TypeError on anything unexpected, and constructs the channel. There is no token, no shared secret, no TLS, no peer allowlist anywhere in the module. Connecting to the actor-pool port is the authorisation.

One configuration flag is worth flagging because it changes the blast radius. When a pool is started with listen_elastic_ip, the server deliberately rebinds:

if config.pop("listen_elastic_ip", False):
    if is_v6_ip(host):
        _host = "::"
    else:
        _host = "0.0.0.0"

The in-code comment explains the intent honestly — the announced Actor.address must stay the externally reachable one, so the socket has to listen on everything. That is a reasonable answer to a real problem in elastic-IP and NAT deployments. It is also a wildcard bind in front of an unauthenticated cloudpickle.loads.

Why the downstream matters more than the project

Xoscar on its own is a modest repository: 175 stars, 31 forks, Apache-2.0, described as a “Python actor framework for heterogeneous computing,” and carrying a header noting it is derived from Alibaba’s Mars. Treated as a standalone library, a 9.3 here would be a footnote.

It is not standalone. Xinference 4.0.0 declares xoscar>=0.10.0 as a hard requirement, and the xorbitsai/inference repository carries 9,611 stars and 877 forks. Xinference is a general-purpose inference server — local LLMs, embeddings, rerankers, image and audio models — typically run as a supervisor process with one or more worker processes on GPU hosts, which is exactly what an actor framework is there to connect. The dependency is transitive, unpinned at the lower bound, and invisible on any dashboard that tracks only direct dependencies. Most teams running Xinference have never typed the word xoscar.

We want to be careful about the exposure claim, because the CVE text is more confident than we are. It states the process “commonly runs as root on GPU nodes.” That is true of a lot of containerised GPU serving and false of plenty of well-run clusters, and we did not measure it. What we can say from the source is narrower and sufficient: the actor-pool channel performs no authentication, accepts pickled data, and reconstructs it before any message-type check; whether your particular deployment puts that port on an interface an attacker can reach is a question about your network, not about Xoscar. The honest threat model is not “internet-facing RCE” by default — it is anyone with east-west access to the serving namespace owns the inference process, which in a flat Kubernetes network or a shared research cluster is a much larger set of people than the ones who can reach your API gateway.

The pattern this belongs to

Distributed Python frameworks built for trusted-cluster use keep producing this record. The design assumption — every node in the pool is a peer, so serialisation can be as fast and as permissive as the language allows — was defensible when the pool lived on one rack behind one firewall. Model serving moved that pool into multi-tenant clusters, into cloud VPCs shared with other workloads, and into agent platforms where the inference tier is addressable by application code that handles untrusted input. The transport did not change with it.

This is the second unauthenticated-pickle record in the model-serving tier this month with no fixed version attached. The ten LightLLM CVEs over an exposed RPyC channel landed the same way, and we saw the same shape again in NVIDIA’s October bulletins, where two Megatron CWE-502 checkpoint-loading RCEs sat alongside a 10.0. The awkward observation holds across all three: in this ecosystem, deserialising something another process produced is not an edge case, it is the normal mode of operation. Pickle-based transport is a decision that is cheap to make, extremely expensive to reverse, and nearly impossible to audit from outside the project, because nothing in the public API hints that a socket read is an execution primitive.

The structural fix is well known and unglamorous: a non-executable wire format for the control plane, or a mandatory authenticated handshake before any payload is deserialised, with the key distributed when the pool is provisioned. Xoscar already carries a fury code path alongside cloudpickle in unpickle_buffers, so the serialiser is not hard-wired — but the selection is made by the sender, inside the message, which is the wrong end of the connection to be trusting.

What to do

  • Find out whether you are running it. pip show xoscar, or search your lockfiles. If you run Xinference, Xorbits, or anything that pulled them in, you have it. There is no upgrade that closes this today — 0.11.2 is both the newest release and the top of the affected range.
  • Treat the actor-pool port as a control-plane port. Bind it to a private interface, never a wildcard, and never with listen_elastic_ip unless a network policy is enforcing who can reach it. In Kubernetes that means a NetworkPolicy restricting the supervisor and worker ports to the serving namespace; on a VM, a host firewall rule. This is the only mitigation available right now and it is a complete one if applied properly, because the bug requires reaching the socket.
  • Do not rely on the API gateway as the boundary. The vulnerable port is not the HTTP endpoint you authenticate. Anything that can open a TCP connection to the worker — a compromised sidecar, a misconfigured service mesh, another tenant’s pod, a developer’s port-forward left running — is in scope.
  • Drop privileges on the serving process. If the CVE’s root-on-GPU-node assumption describes your deployment, fix that regardless of this record. Run the container as a non-root user, with a read-only root filesystem and no capability set, so that a pickle gadget lands somewhere constrained.
  • Watch for a release rather than a patch note. With no advisory filed on the repository, pip audit and most SCA tooling will surface this only through the NVD record, and only for projects that resolve transitive dependencies. Pin the check yourself.

Verification note: the CVE identifier, publication timestamp, CVSS 4.0 vector and base score, CWE-502 assignment, affected range, vulnStatus and VulnCheck CNA attribution were read first-hand from the NVD 2.0 API. The source excerpts above were fetched directly from the xorbitsai/xoscar repository at tag v0.11.2 — python/xoscar/backends/communication/socket.py for recv(), handle_connection, on_connected and the listen_elastic_ip rebind, and python/xoscar/serialization/core.pyx for unpickle_buffers — and are verbatim, including the in-code comment. Release tags, publication dates, repository metadata (175 stars, 31 forks, Apache-2.0, not archived), the absence of commits after the v0.11.2 tag and the absence of any repository security advisory were read first-hand from the GitHub API; the PyPI JSON API confirmed xoscar 0.11.2 as current (uploaded 7 October 2026 02:08 UTC) and Xinference 4.0.0’s requirement of xoscar>=0.10.0. Star and fork counts for xorbitsai/inference were read from the GitHub API on 14 October 2026. We deployed no actor pool, opened no connection to any Xoscar port, crafted no pickle payload and executed no proof of concept; the mechanism described is read from source, and we have explicitly declined to adopt the CVE text’s claim about root privileges as our own because we did not measure it. Brave Search returned only CVE-mirror aggregators for this identifier and no analytical coverage.

Sources: