CVE-2026-49950 is a critical remote code execution bug in Xoscar, the Python actor framework that the Xinference model-serving platform is built on. Anyone who can reach an Xoscar actor pool over TCP can run code as the process that owns it. No authentication is needed. As of 14 October 2026 there is no fixed release, and the most common Python dependency scanners don't flag it.
This post is for teams running Xinference, or anything else built on Xoscar, on GPU servers or in Kubernetes. It covers what the bug is, why the default network settings matter more than usual, a script that finds exposed actor-pool listeners on a host, and what to do until a patch ships.
The short answer
CVE-2026-49950 affects Xoscar 0.1.0 through 0.11.2, which is the latest release. It has no fix yet. The actor pool's TCP channel passes incoming bytes to cloudpickle.loads() before checking what kind of message they are. Unpickling attacker-controlled data runs attacker-chosen code. VulnCheck published the CVE on 13 October 2026, with CVSS 9.8 (v3.1) and 9.3 (v4.0). The CVE record credits Valentin Lobstein (Chocapikk) with the finding. Until a patched release exists, the only defence is network isolation: make sure nothing outside the cluster can connect to any actor-pool port.
| CVE | CVE-2026-49950 (GHSA-5rfg-55mw-4h28) |
| Component | xoscar on PyPI, used by xinference (which requires xoscar>=0.10.0) |
| Affected | 0.1.0 through 0.11.2 (released 7 October 2026) |
| Fixed in | None as of 14 October 2026 |
| Weakness | CWE-502, deserialization of untrusted data |
| Precondition | Network access to an actor-pool port |
What goes wrong in the actor pool
Xoscar lets Python processes on one or many machines call each other's "actors" as if they were local objects. Each process runs an actor pool that listens on a TCP port, and the pools exchange messages over that socket.
Messages are serialized with cloudpickle. In the 0.11.2 source, SocketChannel.recv() reads a header and buffers from the socket and hands them straight to deserialize(). For pickled payloads that ends in unpickle_buffers(), which calls cloudpickle.loads(buffers[0], ...). Nothing before that point checks who is sending or what they are allowed to send.
Pickle is not a data format in the safe sense. A pickle stream can instruct the loader to call any importable function with any arguments, which is how objects define their own reconstruction (__reduce__). So pickle.loads() on untrusted input is equivalent to eval() on untrusted input. Python's own documentation says never to unpickle data from an untrusted source, and our guide to pickle deserialization risk covers the mechanics.
The advisory's key detail is the ordering. The gadget runs during deserialization, before Xoscar inspects the message type. Validating the message afterwards can't help, because by then the attacker's code has already run. The CVE record adds that the server process "commonly runs as root on GPU nodes", which makes the impact a full host compromise on many deployments.
We are not publishing a proof of concept. The description above is the level of detail already in the public advisory, and there is no patch to point people to yet.
Why the defaults matter here
Whether you are exposed depends almost entirely on where the actor pools listen. We read Xinference's command-line code and ran Xoscar 0.11.2 locally to check.
Distributed mode listens on every interface by default. xinference-supervisor and xinference-worker default --host to 0.0.0.0. The supervisor's actor pool binds to host:<supervisor-port>, and if you don't pass --supervisor-port, it picks a free port at random. Workers do the same with --worker-port.
Local mode is safer, until you change it. xinference-local defaults to 127.0.0.1, and its actor pool binds to that host plus a random port. But Xinference's Docker instructions run xinference-local -H 0.0.0.0 and say the flag can't be omitted, because otherwise the API inside the container is unreachable. That flag also moves the unauthenticated actor pool onto every interface. With plain docker run -p 9997:9997, only the API port is published on the host. The actor pool is still reachable from other containers on the same Docker network, from anything that can reach the pod IP in Kubernetes, and from the network itself if you use host networking.
There is more than one port. When we created a pool with two sub-processes on 0.0.0.0, the main process and each sub-process opened their own listener:
$ lsof -nP -iTCP -sTCP:LISTEN | grep -i python
Python 87460 ... TCP *:35741 (LISTEN)
Python 87527 ... TCP *:54313 (LISTEN)
Python 87528 ... TCP *:54314 (LISTEN)
Random ports on all interfaces are a bad combination for firewalling. A rule that blocks "the Xinference port" doesn't cover them, and a port scan finds them anyway.
Your scanner probably says you're fine
The GitHub advisory for CVE-2026-49950 is unreviewed and lists no affected package at all. The CVE record identifies the product only by its GitHub repository (github.com/xorbitsai/xoscar), not as a PyPI package. OSV, which feeds pip-audit, returned no result for xoscar 0.11.2 when we queried it on 14 October. We ran pip-audit against a requirements file pinning the vulnerable version:
$ echo "xoscar==0.11.2" > req.txt
$ pip-audit -r req.txt --no-deps --disable-pip
No known vulnerabilities found
This will probably change once the advisory is reviewed and mapped to the PyPI package. Until then, a clean scan says nothing about this bug. If you run Xinference, assume you're affected and check by hand.
Find exposed actor pools on a host
This script finds processes that have Xoscar loaded (by its native extension in the process's memory map, or xinference/xoscar on the command line), lists their TCP listeners, and flags any that aren't on loopback. It needs psutil. Run it as root to see other users' processes.
#!/usr/bin/env python3
"""List TCP listeners opened by processes that have Xoscar loaded (Xinference and
other Xoscar apps), and flag any that are not bound to loopback.
Needs psutil (pip install psutil). Run as root to see other users' processes."""
import ipaddress, subprocess, psutil
def mapped_files(p):
try:
return [m.path for m in p.memory_maps()] # Linux, Windows
except AttributeError: # macOS: ask lsof
out = subprocess.run(["lsof", "-Fn", "-p", str(p.pid)], capture_output=True, text=True).stdout
return [l[1:] for l in out.splitlines() if l.startswith("n")]
def has_xoscar(p):
try:
if any("xoscar" in f for f in mapped_files(p)):
return True
except (psutil.AccessDenied, psutil.NoSuchProcess):
pass
try:
return any("xinference" in a or "xoscar" in a for a in p.cmdline())
except (psutil.AccessDenied, psutil.NoSuchProcess):
return False
exposed = 0
for p in psutil.process_iter(["pid", "name", "username"]):
if not has_xoscar(p):
continue
try:
listens = [c for c in p.net_connections(kind="tcp") if c.status == psutil.CONN_LISTEN]
except (psutil.AccessDenied, psutil.NoSuchProcess):
print(f"pid {p.pid}: access denied, rerun as root"); continue
for c in listens:
ip = c.laddr.ip
local = ip not in ("0.0.0.0", "::") and ipaddress.ip_address(ip.split("%")[0]).is_loopback
exposed += not local
print(f"{'ok ' if local else 'EXPOSED '} pid {p.pid:<7} user {p.info['username']:<12} {ip}:{c.laddr.port}")
print(f"\n{exposed} listener(s) reachable from other hosts")
raise SystemExit(1 if exposed else 0)
We tested it with two Xoscar 0.11.2 pools running side by side, one on 0.0.0.0 with two sub-processes and one on 127.0.0.1 with one:
EXPOSED pid 90173 user dev 0.0.0.0:10218
ok pid 90175 user dev 127.0.0.1:16744
ok pid 90181 user dev 127.0.0.1:54361
EXPOSED pid 90182 user dev 0.0.0.0:54359
EXPOSED pid 90183 user dev 0.0.0.0:54360
3 listener(s) reachable from other hosts
"EXPOSED" means bound to all interfaces, not necessarily reachable from the internet. A cloud security group or host firewall may still block it, so check that next. In a distributed cluster, the supervisor and workers have to reach each other, so some exposure on the private network is expected. The question is whether anything else can connect.
What to do until there is a patch
- Single-node Xinference: keep the default
127.0.0.1on bare metal. If the API needs to be reachable remotely, put a reverse proxy on the same host in front of port 9997 instead of passing--host 0.0.0.0, and the actor pool stays on loopback. In Docker, where-H 0.0.0.0is required, publish only port 9997, avoid host networking, and keep the container off shared networks with untrusted workloads. - Distributed Xinference: bind to the private interface, not
0.0.0.0. Pass--host <private-ip>to the supervisor and workers, and fix their ports with--supervisor-portand--worker-portso they are predictable. - Firewall by source, not by port. Sub-process ports are random, so allow inbound traffic to cluster nodes only from the other cluster nodes, and allow everyone else only to the API port. In Kubernetes, a NetworkPolicy expresses exactly that:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: xinference-isolate
namespace: inference
spec:
podSelector:
matchLabels:
app: xinference
policyTypes: ["Ingress"]
ingress:
- from: # supervisor and workers: any port, peers only
- podSelector:
matchLabels:
app: xinference
- from: # everyone else: the REST API only
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 9997
Adjust the labels and the ingress namespace to your setup, and check that your CNI actually enforces NetworkPolicy, because some don't. We validated the manifest's structure but haven't run it against a live cluster.
4. Don't run the server as root. Container images that run model servers as root turn this bug into host compromise. A non-root user doesn't fix the bug, but it limits what the injected code can do.
5. Remember shared GPU boxes. Binding to 127.0.0.1 stops other machines, not other users on the same machine. On a shared research server, any local account can connect to a loopback actor pool.
6. Watch for the fix. Track xorbitsai/xoscar releases. Because your scanner won't warn you yet, add a manual check to CI for anything that installs Xinference, for example python -c "import importlib.metadata as m; print(m.version('xoscar'))", and treat 0.11.2 or older as vulnerable.
The pattern behind it
Distributed Python frameworks keep making the same trade. Inside a "trusted" cluster, pickling arbitrary objects between processes is convenient, and authentication feels like overhead. Ray took the same position for its job API: CVE-2023-48022 is marked disputed in NVD because the vendor says Ray is not meant to be used outside a strictly controlled network. That is a legitimate design choice. But it means the network boundary is the only control, and GPU clusters are often set up quickly by people optimising for throughput, with 0.0.0.0 in the quick-start command.
It's also worth being fair to the project. Xoscar is an internal transport, not a public API, and nothing in its design suggested it should face untrusted networks. The bug is real because deployments put it there. Xinference has already fixed one critical code execution bug this year (CVE-2026-61539, an eval() in tool-call parsing, fixed in 2.7.0). Expect an Xoscar fix to follow, and plan for the next one by assuming the actor pools are a root shell for anyone who can reach them.
For a dependency like this, the useful question isn't just "which version do we run?" but "which of our services pull it in, and where are they reachable?" Answering that across every repository at once is what Safeguard SCA does. Keep in mind that every tool, ours included, depends on advisories that can lag the disclosure, as this one does.